summaryrefslogtreecommitdiff
path: root/core/java/android
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android')
-rw-r--r--core/java/android/permission/PermGroupUsage.java79
-rw-r--r--core/java/android/permission/PermissionManager.java18
-rw-r--r--core/java/android/permission/PermissionUsageHelper.java441
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 87f3764f5ca0..705ac1b5138f 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);
+ }
+ }
+}