Merge "Add most basic aspects of permission usage to PermissionManager"

This commit is contained in:
Nate Myren
2021-01-06 19:58:25 +00:00
committed by Android (Google) Code Review
3 changed files with 538 additions and 0 deletions

View File

@@ -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;
}
}

View File

@@ -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;
@@ -853,6 +855,22 @@ public final class PermissionManager {
return mSplitPermissionInfos;
}
/**
* @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

View File

@@ -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);
}
}
}