diff options
| author | Nate Myren <ntmyren@google.com> | 2020-12-07 13:10:09 -0800 |
|---|---|---|
| committer | Nate Myren <ntmyren@google.com> | 2020-12-16 08:40:09 -0800 |
| commit | 32c2514a9ddeca323e4c1f102257555548845ffe (patch) | |
| tree | ad62ab366a9614f563ae3e3fef53891c5b5d99ae /core/java/android | |
| parent | c51d98c112ac0dbe0ebd12f4fb497c7c253533bd (diff) | |
Add most basic aspects of permission usage to PermissionManager
Add the basic API for getting permission usage for mic, camera, and
location. Does not use special attribution yet. Mostly untested
Bug: 172868375
Test: Basic manual tests
Change-Id: Icb268b820557d62125e9307d6ffcf7046ab9b490
Diffstat (limited to 'core/java/android')
| -rw-r--r-- | core/java/android/permission/PermGroupUsage.java | 79 | ||||
| -rw-r--r-- | core/java/android/permission/PermissionManager.java | 18 | ||||
| -rw-r--r-- | core/java/android/permission/PermissionUsageHelper.java | 441 |
3 files changed, 538 insertions, 0 deletions
diff --git a/core/java/android/permission/PermGroupUsage.java b/core/java/android/permission/PermGroupUsage.java new file mode 100644 index 000000000000..3bee401dbd0d --- /dev/null +++ b/core/java/android/permission/PermGroupUsage.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.permission; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +/** + * Represents the usage of a permission group by an app. Supports package name, user, permission + * group, whether or not the access is running or recent, whether the access is tied to a phone + * call, and an optional special attribution. + * + * @hide + */ +public final class PermGroupUsage { + + private final String mPackageName; + private final int mUid; + private final String mPermGroupName; + private final boolean mIsActive; + private final boolean mIsPhoneCall; + private final CharSequence mAttribution; + + PermGroupUsage(@NonNull String packageName, int uid, + @NonNull String permGroupName, boolean isActive, boolean isPhoneCall, + @Nullable CharSequence attribution) { + this.mPackageName = packageName; + this.mUid = uid; + this.mPermGroupName = permGroupName; + this.mIsActive = isActive; + this.mIsPhoneCall = isPhoneCall; + this.mAttribution = attribution; + } + + public @NonNull String getPackageName() { + return mPackageName; + } + + public int getUid() { + return mUid; + } + + public @NonNull String getPermGroupName() { + return mPermGroupName; + } + + public boolean isActive() { + return mIsActive; + } + + public boolean isPhoneCall() { + return mIsPhoneCall; + } + + public @Nullable CharSequence getAttribution() { + return mAttribution; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + + "packageName: " + mPackageName + ", UID: " + mUid + ", permGroup: " + + mPermGroupName + ", isActive: " + mIsActive + ",attribution: " + mAttribution; + } +} diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index d31e0129fb27..4aa3670babc8 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -41,6 +41,7 @@ import android.content.pm.ParceledListSlice; import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.content.pm.permission.SplitPermissionInfoParcelable; +import android.media.AudioManager; import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -114,6 +115,7 @@ public final class PermissionManager { private final ArrayMap<PackageManager.OnPermissionsChangedListener, IOnPermissionsChangeListener> mPermissionListeners = new ArrayMap<>(); + private PermissionUsageHelper mUsageHelper; private List<SplitPermissionInfo> mSplitPermissionInfos; @@ -854,6 +856,22 @@ public final class PermissionManager { } /** + * @return A list of permission groups currently or recently used by all apps by all users in + * the current profile group. + * + * @hide + */ + @NonNull + @RequiresPermission(Manifest.permission.GET_APP_OPS_STATS) + public List<PermGroupUsage> getIndicatorAppOpUsageData() { + // Lazily initialize the usage helper + if (mUsageHelper == null) { + mUsageHelper = new PermissionUsageHelper(mContext); + } + return mUsageHelper.getOpUsageData(new AudioManager().isMicrophoneMute()); + } + + /** * Gets the list of packages that have permissions that specified * {@code requestDontAutoRevokePermissions=true} in their * {@code application} manifest declaration. diff --git a/core/java/android/permission/PermissionUsageHelper.java b/core/java/android/permission/PermissionUsageHelper.java new file mode 100644 index 000000000000..6a8dca153585 --- /dev/null +++ b/core/java/android/permission/PermissionUsageHelper.java @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.permission; + +import static android.Manifest.permission_group.CAMERA; +import static android.Manifest.permission_group.LOCATION; +import static android.Manifest.permission_group.MICROPHONE; +import static android.app.AppOpsManager.OPSTR_CAMERA; +import static android.app.AppOpsManager.OPSTR_COARSE_LOCATION; +import static android.app.AppOpsManager.OPSTR_FINE_LOCATION; +import static android.app.AppOpsManager.OPSTR_PHONE_CALL_CAMERA; +import static android.app.AppOpsManager.OPSTR_PHONE_CALL_MICROPHONE; +import static android.app.AppOpsManager.OPSTR_RECORD_AUDIO; +import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED; +import static android.app.AppOpsManager.opToPermission; +import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED; + +import android.annotation.NonNull; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.LocationManager; +import android.os.Process; +import android.os.UserHandle; +import android.provider.DeviceConfig; +import android.util.ArrayMap; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * A helper which gets all apps which have used microphone, camera, and possible location + * permissions within a certain timeframe, as well as possible special attributions, and if the + * usage is a phone call. + * + * @hide + */ +public class PermissionUsageHelper { + + /** Whether to show the mic and camera icons. */ + private static final String PROPERTY_CAMERA_MIC_ICONS_ENABLED = "camera_mic_icons_enabled"; + + /** Whether to show the location indicators. */ + private static final String PROPERTY_LOCATION_INDICATORS_ENABLED = + "location_indicators_enabled"; + + /** How long after an access to show it as "recent" */ + private static final String RECENT_ACCESS_TIME_MS = "recent_acccess_time_ms"; + + /** How long after an access to show it as "running" */ + private static final String RUNNING_ACCESS_TIME_MS = "running_acccess_time_ms"; + + private static final long DEFAULT_RUNNING_TIME_MS = 5000L; + private static final long DEFAULT_RECENT_TIME_MS = 30000L; + + private static boolean shouldShowIndicators() { + return true; + // TODO ntmyren: remove true set when device config is configured correctly + //DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, + //PROPERTY_CAMERA_MIC_ICONS_ENABLED, true); + } + + private static boolean shouldShowLocationIndicator() { + return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, + PROPERTY_LOCATION_INDICATORS_ENABLED, false); + } + + private static long getRecentThreshold(Long now) { + return now - DeviceConfig.getLong(DeviceConfig.NAMESPACE_PRIVACY, + RECENT_ACCESS_TIME_MS, DEFAULT_RECENT_TIME_MS); + } + + private static long getRunningThreshold(Long now) { + return now - DeviceConfig.getLong(DeviceConfig.NAMESPACE_PRIVACY, + RUNNING_ACCESS_TIME_MS, DEFAULT_RUNNING_TIME_MS); + } + + private static final List<String> LOCATION_OPS = List.of( + OPSTR_COARSE_LOCATION, + OPSTR_FINE_LOCATION + ); + + private static final List<String> MIC_OPS = List.of( + OPSTR_PHONE_CALL_CAMERA, + OPSTR_RECORD_AUDIO + ); + + private static final List<String> CAMERA_OPS = List.of( + OPSTR_PHONE_CALL_CAMERA, + OPSTR_CAMERA + ); + + private static @NonNull String getGroupForOp(String op) { + switch(op) { + case OPSTR_RECORD_AUDIO: + return MICROPHONE; + case OPSTR_CAMERA: + return CAMERA; + case OPSTR_PHONE_CALL_MICROPHONE: + case OPSTR_PHONE_CALL_CAMERA: + return op; + case OPSTR_COARSE_LOCATION: + case OPSTR_FINE_LOCATION: + return LOCATION; + default: + throw new IllegalArgumentException("Unknown app op: " + op); + } + } + + private Context mContext; + private Map<UserHandle, Context> mUserContexts; + private PackageManager mPkgManager; + private AppOpsManager mAppOpsManager; + + /** + * Constructor for PermissionUsageHelper + * @param context The context from which to derive the package information + */ + public PermissionUsageHelper(Context context) { + mContext = context; + mPkgManager = context.getPackageManager(); + mAppOpsManager = context.getSystemService(AppOpsManager.class); + mUserContexts = Map.of(Process.myUserHandle(), mContext); + } + + private Context getUserContext(UserHandle user) { + if (!(mUserContexts.containsKey(user))) { + mUserContexts.put(user, mContext.createContextAsUser(user, 0)); + } + return mUserContexts.get(user); + } + + /** + * @see PermissionManager.getIndicatorAppOpUsageData + */ + public List<PermGroupUsage> getOpUsageData(boolean isMicMuted) { + if (!shouldShowIndicators()) { + return null; + } + + List<String> ops = CAMERA_OPS; + if (shouldShowLocationIndicator()) { + ops.addAll(LOCATION_OPS); + } + if (!isMicMuted) { + ops.addAll(MIC_OPS); + } + + Map<String, List<OpUsage>> rawUsages = getOpUsages(ops); + Map<PackageAttribution, CharSequence> packagesWithAttributionLabels = + getTrustedAttributions(rawUsages.get(MICROPHONE)); + + List<PermGroupUsage> usages = new ArrayList<>(); + List<String> usedPermGroups = new ArrayList<>(rawUsages.keySet()); + for (int permGroupNum = 0; permGroupNum < usedPermGroups.size(); permGroupNum++) { + boolean isPhone = false; + String permGroup = usedPermGroups.get(permGroupNum); + if (permGroup.equals(OPSTR_PHONE_CALL_MICROPHONE)) { + isPhone = true; + permGroup = MICROPHONE; + } else if (permGroup.equals(OPSTR_PHONE_CALL_CAMERA)) { + isPhone = true; + permGroup = CAMERA; + } + + int numUsages = rawUsages.get(permGroup).size(); + for (int usageNum = 0; usageNum < numUsages; usageNum++) { + OpUsage usage = rawUsages.get(permGroup).get(usageNum); + usages.add(new PermGroupUsage(usage.packageName, usage.uid, permGroup, + usage.isRunning, isPhone, + packagesWithAttributionLabels.get(usage.toPackageAttr()))); + } + } + + return usages; + } + + /** + * Get the raw usages from the system, and then parse out the ones that are not recent enough, + * determine which permission group each belongs in, and removes duplicates (if the same app + * uses multiple permissions of the same group). Stores the package name, attribution tag, user, + * running/recent info, if the usage is a phone call, per permission group. + * + * @param opNames a list of op names to get usage for + * + * @return A map of permission group -> list of usages that are recent or running + */ + private Map<String, List<OpUsage>> getOpUsages(List<String> opNames) { + List<AppOpsManager.PackageOps> ops; + try { + ops = mAppOpsManager.getPackagesForOps(opNames.toArray(new String[opNames.size()])); + } catch (NullPointerException e) { + // older builds might not support all the app-ops requested + return Collections.emptyMap(); + } + + long now = System.currentTimeMillis(); + long recentThreshold = getRecentThreshold(now); + long runningThreshold = getRunningThreshold(now); + int opFlags = OP_FLAGS_ALL_TRUSTED; + Map<String, Map<PackageAttribution, OpUsage>> usages = new ArrayMap<>(); + + int numPkgOps = ops.size(); + for (int pkgOpNum = 0; pkgOpNum < numPkgOps; pkgOpNum++) { + AppOpsManager.PackageOps pkgOps = ops.get(pkgOpNum); + int uid = pkgOps.getUid(); + UserHandle user = UserHandle.getUserHandleForUid(uid); + String packageName = pkgOps.getPackageName(); + + int numOpEntries = pkgOps.getOps().size(); + for (int opEntryNum = 0; opEntryNum < numOpEntries; opEntryNum++) { + AppOpsManager.OpEntry opEntry = pkgOps.getOps().get(opEntryNum); + String op = opEntry.getOpStr(); + List<String> attributionTags = + new ArrayList<>(opEntry.getAttributedOpEntries().keySet()); + + int numAttrEntries = opEntry.getAttributedOpEntries().size(); + for (int attrOpEntryNum = 0; attrOpEntryNum < numAttrEntries; attrOpEntryNum++) { + String attributionTag = attributionTags.get(attrOpEntryNum); + AppOpsManager.AttributedOpEntry attrOpEntry = + opEntry.getAttributedOpEntries().get(attributionTag); + + long lastAccessTime = attrOpEntry.getLastAccessTime(opFlags); + if (lastAccessTime < recentThreshold) { + continue; + } + if (!isUserSensitive(packageName, user, op) + && !isLocationProvider(packageName, user)) { + continue; + } + + boolean isRunning = attrOpEntry.isRunning() + || lastAccessTime >= runningThreshold; + + OpUsage proxyUsage = null; + AppOpsManager.OpEventProxyInfo proxy = attrOpEntry.getLastProxyInfo(opFlags); + if (proxy != null && proxy.getPackageName() != null) { + proxyUsage = new OpUsage(proxy.getPackageName(), proxy.getAttributionTag(), + uid, lastAccessTime, isRunning, null); + } + + String permGroupName = getGroupForOp(op); + OpUsage usage = new OpUsage(packageName, attributionTag, uid, + lastAccessTime, isRunning, proxyUsage); + + PackageAttribution packageAttr = usage.toPackageAttr(); + if (!usages.containsKey(permGroupName)) { + ArrayMap<PackageAttribution, OpUsage> map = new ArrayMap<>(); + map.put(packageAttr, usage); + usages.put(permGroupName, map); + } else { + Map<PackageAttribution, OpUsage> permGroupUsages = + usages.get(permGroupName); + if (!permGroupUsages.containsKey(packageAttr)) { + permGroupUsages.put(packageAttr, usage); + } else if (usage.lastAccessTime + > permGroupUsages.get(packageAttr).lastAccessTime) { + permGroupUsages.put(packageAttr, usage); + } + } + } + } + } + + Map<String, List<OpUsage>> flattenedUsages = new ArrayMap<>(); + List<String> permGroups = new ArrayList<>(usages.keySet()); + for (int i = 0; i < permGroups.size(); i++) { + String permGroupName = permGroups.get(i); + flattenedUsages.put(permGroupName, new ArrayList<>(usages.get(permGroupName).values())); + } + return flattenedUsages; + } + + // TODO ntmyren: create JavaDoc and copy merging of proxy chains and trusted labels from + // "usages" livedata in ReviewOngoingUsageLiveData + private Map<PackageAttribution, CharSequence> getTrustedAttributions(List<OpUsage> usages) { + ArrayMap<PackageAttribution, CharSequence> attributions = new ArrayMap<>(); + if (usages == null) { + return attributions; + } + Set<List<OpUsage>> proxyChains = getProxyChains(usages); + Map<Pair<String, UserHandle>, CharSequence> trustedLabels = getTrustedAttributionLabels(); + + + return attributions; + } + + // TODO ntmyren: create JavaDoc and copy proxyChainsLiveData from ReviewOngoingUsageLiveData + private Set<List<OpUsage>> getProxyChains(List<OpUsage> usages) { + Map<PackageAttribution, List<OpUsage>> inProgressChains = new ArrayMap<>(); + List<OpUsage> remainingUsages = new ArrayList<>(usages); + // find all one-link chains (that is, all proxied apps whose proxy is not included in + // the usage list) + for (int usageNum = 0; usageNum < usages.size(); usageNum++) { + OpUsage usage = usages.get(usageNum); + PackageAttribution usageAttr = usage.toPackageAttr(); + if (usage.proxy == null) { + continue; + } + PackageAttribution proxyAttr = usage.proxy.toPackageAttr(); + boolean proxyExists = false; + for (int otherUsageNum = 0; otherUsageNum < usages.size(); otherUsageNum++) { + if (usages.get(otherUsageNum).toPackageAttr().equals(proxyAttr)) { + proxyExists = true; + break; + } + } + + if (!proxyExists) { + inProgressChains.put(usageAttr, List.of(usage)); + remainingUsages.remove(usage); + } + } + + // find all possible starting points for chains + for (int i = 0; i < usages.size(); i++) { + OpUsage usage = usages.get(i); + } + + /* + // find all possible starting points for chains + for (usage in remainingProxyChainUsages.toList()) { + // if this usage has no proxy, but proxies another usage, it is the start of a chain + val usageAttr = getPackageAttr(usage) + if (usage.proxyAccess == null && remainingProxyChainUsages.any { + it.proxyAccess != null && getPackageAttr(it.proxyAccess) == usageAttr + }) { + inProgressChains[usageAttr] = mutableListOf(usage) + } + + // if this usage is a chain start, or no usage have this usage as a proxy, remove it + if (usage.proxyAccess == null) { + remainingProxyChainUsages.remove(usage) + } + } + + */ + + return null; + } + + // TODO ntmyren: create JavaDoc and copy trustedAttrsLiveData from ReviewOngoingUsageLiveData + private Map<Pair<String, UserHandle>, CharSequence> getTrustedAttributionLabels() { + return new ArrayMap<>(); + } + + private boolean isUserSensitive(String packageName, UserHandle user, String op) { + if (op.equals(OPSTR_PHONE_CALL_CAMERA) || op.equals(OPSTR_PHONE_CALL_MICROPHONE)) { + return true; + } + + if (opToPermission(op) == null) { + return false; + } + + int permFlags = mPkgManager.getPermissionFlags(opToPermission(op), packageName, user); + return (permFlags & FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED) != 0; + } + + private boolean isLocationProvider(String packageName, UserHandle user) { + return getUserContext(user) + .getSystemService(LocationManager.class).isProviderPackage(packageName); + } + + /** + * Represents the usage of an App op by a particular package and attribution + */ + private static class OpUsage { + + public final String packageName; + public final String attributionTag; + public final int uid; + public final long lastAccessTime; + public final OpUsage proxy; + public final boolean isRunning; + + OpUsage(String packageName, String attributionTag, int uid, long lastAccessTime, + boolean isRunning, OpUsage proxy) { + this.isRunning = isRunning; + this.packageName = packageName; + this.attributionTag = attributionTag; + this.uid = uid; + this.lastAccessTime = lastAccessTime; + this.proxy = proxy; + } + + public PackageAttribution toPackageAttr() { + return new PackageAttribution(packageName, attributionTag, uid); + } + } + + /** + * A unique identifier for one package attribution, made up of attribution tag, package name + * and user + */ + private static class PackageAttribution { + public final String packageName; + public final String attributionTag; + public final int uid; + + PackageAttribution(String packageName, String attributionTag, int uid) { + this.packageName = packageName; + this.attributionTag = attributionTag; + this.uid = uid; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PackageAttribution)) { + return false; + } + PackageAttribution other = (PackageAttribution) obj; + return Objects.equals(packageName, other.packageName) && Objects.equals(attributionTag, + other.attributionTag) && Objects.equals(uid, other.uid); + } + + @Override + public int hashCode() { + return Objects.hash(packageName, attributionTag, uid); + } + } +} |
