840 lines
36 KiB
Java
840 lines
36 KiB
Java
![]() |
/*
|
||
|
* 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.ATTRIBUTION_CHAIN_ID_NONE;
|
||
|
import static android.app.AppOpsManager.ATTRIBUTION_FLAGS_NONE;
|
||
|
import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
|
||
|
import static android.app.AppOpsManager.ATTRIBUTION_FLAG_RECEIVER;
|
||
|
import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED;
|
||
|
import static android.app.AppOpsManager.AttributionFlags;
|
||
|
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_RECEIVE_AMBIENT_TRIGGER_AUDIO;
|
||
|
import static android.app.AppOpsManager.OPSTR_RECORD_AUDIO;
|
||
|
import static android.app.AppOpsManager.OP_CAMERA;
|
||
|
import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED;
|
||
|
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
|
||
|
import static android.media.AudioSystem.MODE_IN_COMMUNICATION;
|
||
|
import static android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
|
||
|
|
||
|
import android.annotation.NonNull;
|
||
|
import android.annotation.Nullable;
|
||
|
import android.app.AppOpsManager;
|
||
|
import android.companion.virtual.VirtualDevice;
|
||
|
import android.companion.virtual.VirtualDeviceManager;
|
||
|
import android.content.Context;
|
||
|
import android.content.pm.ApplicationInfo;
|
||
|
import android.content.pm.Attribution;
|
||
|
import android.content.pm.PackageInfo;
|
||
|
import android.content.pm.PackageManager;
|
||
|
import android.content.res.Resources;
|
||
|
import android.icu.text.ListFormatter;
|
||
|
import android.location.LocationManager;
|
||
|
import android.media.AudioManager;
|
||
|
import android.os.Process;
|
||
|
import android.os.UserHandle;
|
||
|
import android.permission.flags.Flags;
|
||
|
import android.provider.DeviceConfig;
|
||
|
import android.telephony.TelephonyManager;
|
||
|
import android.util.ArrayMap;
|
||
|
import android.util.ArraySet;
|
||
|
import android.util.Slog;
|
||
|
|
||
|
import com.android.internal.annotations.GuardedBy;
|
||
|
|
||
|
import java.util.ArrayList;
|
||
|
import java.util.Collections;
|
||
|
import java.util.List;
|
||
|
import java.util.Map;
|
||
|
import java.util.Objects;
|
||
|
|
||
|
/**
|
||
|
* 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 implements AppOpsManager.OnOpActiveChangedListener,
|
||
|
AppOpsManager.OnOpStartedListener {
|
||
|
|
||
|
private static final String LOG_TAG = PermissionUsageHelper.class.getName();
|
||
|
|
||
|
/**
|
||
|
* 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_access_time_ms";
|
||
|
|
||
|
/**
|
||
|
* How long after an access to show it as "running"
|
||
|
*/
|
||
|
private static final String RUNNING_ACCESS_TIME_MS = "running_access_time_ms";
|
||
|
|
||
|
private static final String SYSTEM_PKG = "android";
|
||
|
|
||
|
private static final long DEFAULT_RUNNING_TIME_MS = 5000L;
|
||
|
private static final long DEFAULT_RECENT_TIME_MS = 15000L;
|
||
|
|
||
|
private static boolean shouldShowIndicators() {
|
||
|
return 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_MICROPHONE,
|
||
|
OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO,
|
||
|
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_RECEIVE_AMBIENT_TRIGGER_AUDIO:
|
||
|
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 ArrayMap<UserHandle, Context> mUserContexts;
|
||
|
private PackageManager mPkgManager;
|
||
|
private AppOpsManager mAppOpsManager;
|
||
|
private VirtualDeviceManager mVirtualDeviceManager;
|
||
|
@GuardedBy("mAttributionChains")
|
||
|
private final ArrayMap<Integer, ArrayList<AccessChainLink>> mAttributionChains =
|
||
|
new ArrayMap<>();
|
||
|
|
||
|
/**
|
||
|
* Constructor for PermissionUsageHelper
|
||
|
*
|
||
|
* @param context The context from which to derive the package information
|
||
|
*/
|
||
|
public PermissionUsageHelper(@NonNull Context context) {
|
||
|
mContext = context;
|
||
|
mPkgManager = context.getPackageManager();
|
||
|
mAppOpsManager = context.getSystemService(AppOpsManager.class);
|
||
|
mVirtualDeviceManager = context.getSystemService(VirtualDeviceManager.class);
|
||
|
mUserContexts = new ArrayMap<>();
|
||
|
mUserContexts.put(Process.myUserHandle(), mContext);
|
||
|
// TODO ntmyren: make this listen for flag enable/disable changes
|
||
|
String[] opStrs = {OPSTR_CAMERA, OPSTR_RECORD_AUDIO};
|
||
|
mAppOpsManager.startWatchingActive(opStrs, context.getMainExecutor(), this);
|
||
|
int[] ops = {OP_CAMERA, OP_RECORD_AUDIO};
|
||
|
mAppOpsManager.startWatchingStarted(ops, this);
|
||
|
}
|
||
|
|
||
|
private Context getUserContext(UserHandle user) {
|
||
|
if (!(mUserContexts.containsKey(user))) {
|
||
|
mUserContexts.put(user, mContext.createContextAsUser(user, 0));
|
||
|
}
|
||
|
return mUserContexts.get(user);
|
||
|
}
|
||
|
|
||
|
public void tearDown() {
|
||
|
mAppOpsManager.stopWatchingActive(this);
|
||
|
mAppOpsManager.stopWatchingStarted(this);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onOpActiveChanged(@NonNull String op, int uid, @NonNull String packageName,
|
||
|
boolean active) {
|
||
|
// not part of an attribution chain. Do nothing
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onOpActiveChanged(@NonNull String op, int uid, @NonNull String packageName,
|
||
|
@Nullable String attributionTag, boolean active, @AttributionFlags int attributionFlags,
|
||
|
int attributionChainId) {
|
||
|
if (active) {
|
||
|
// Started callback handles these
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// if any link in the chain is finished, remove the chain. Then, find any other chains that
|
||
|
// contain this op/package/uid/tag combination, and remove them, as well.
|
||
|
// TODO ntmyren: be smarter about this
|
||
|
synchronized (mAttributionChains) {
|
||
|
mAttributionChains.remove(attributionChainId);
|
||
|
int numChains = mAttributionChains.size();
|
||
|
ArrayList<Integer> toRemove = new ArrayList<>();
|
||
|
for (int i = 0; i < numChains; i++) {
|
||
|
int chainId = mAttributionChains.keyAt(i);
|
||
|
ArrayList<AccessChainLink> chain = mAttributionChains.valueAt(i);
|
||
|
int chainSize = chain.size();
|
||
|
for (int j = 0; j < chainSize; j++) {
|
||
|
AccessChainLink link = chain.get(j);
|
||
|
if (link.packageAndOpEquals(op, packageName, attributionTag, uid)) {
|
||
|
toRemove.add(chainId);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
mAttributionChains.removeAll(toRemove);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onOpStarted(int op, int uid, String packageName, String attributionTag,
|
||
|
@AppOpsManager.OpFlags int flags, @AppOpsManager.Mode int result) {
|
||
|
// not part of an attribution chain. Do nothing
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onOpStarted(int op, int uid, String packageName, String attributionTag,
|
||
|
@AppOpsManager.OpFlags int flags, @AppOpsManager.Mode int result,
|
||
|
@StartedType int startedType, @AttributionFlags int attributionFlags,
|
||
|
int attributionChainId) {
|
||
|
if (startedType == START_TYPE_FAILED || attributionChainId == ATTRIBUTION_CHAIN_ID_NONE
|
||
|
|| attributionFlags == ATTRIBUTION_FLAGS_NONE
|
||
|
|| (attributionFlags & ATTRIBUTION_FLAG_TRUSTED) == 0) {
|
||
|
// If this is not a successful start, or it is not a chain, or it is untrusted, return
|
||
|
return;
|
||
|
}
|
||
|
synchronized (mAttributionChains) {
|
||
|
addLinkToChainIfNotPresentLocked(AppOpsManager.opToPublicName(op), packageName, uid,
|
||
|
attributionTag, attributionFlags, attributionChainId);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void addLinkToChainIfNotPresentLocked(String op, String packageName, int uid,
|
||
|
String attributionTag, int attributionFlags, int attributionChainId) {
|
||
|
|
||
|
ArrayList<AccessChainLink> currentChain = mAttributionChains.computeIfAbsent(
|
||
|
attributionChainId, k -> new ArrayList<>());
|
||
|
AccessChainLink link = new AccessChainLink(op, packageName, attributionTag, uid,
|
||
|
attributionFlags);
|
||
|
|
||
|
if (currentChain.contains(link)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
int currSize = currentChain.size();
|
||
|
if (currSize == 0 || link.isEnd() || !currentChain.get(currSize - 1).isEnd()) {
|
||
|
// if the list is empty, this link is the end, or the last link in the current chain
|
||
|
// isn't the end, add it to the end
|
||
|
currentChain.add(link);
|
||
|
} else if (link.isStart()) {
|
||
|
currentChain.add(0, link);
|
||
|
} else if (currentChain.get(currentChain.size() - 1).isEnd()) {
|
||
|
// we already have the end, and this is a mid node, so insert before the end
|
||
|
currentChain.add(currSize - 1, link);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return Op usage for CAMERA, LOCATION AND MICROPHONE for all packages for a device.
|
||
|
* The returned data is to power privacy indicator.
|
||
|
*/
|
||
|
public @NonNull List<PermissionGroupUsage> getOpUsageDataByDevice(
|
||
|
boolean includeMicrophoneUsage, String deviceId) {
|
||
|
List<PermissionGroupUsage> usages = new ArrayList<>();
|
||
|
|
||
|
if (!shouldShowIndicators()) {
|
||
|
return usages;
|
||
|
}
|
||
|
|
||
|
List<String> ops = new ArrayList<>(CAMERA_OPS);
|
||
|
if (shouldShowLocationIndicator()) {
|
||
|
ops.addAll(LOCATION_OPS);
|
||
|
}
|
||
|
if (includeMicrophoneUsage) {
|
||
|
ops.addAll(MIC_OPS);
|
||
|
}
|
||
|
|
||
|
Map<String, List<OpUsage>> rawUsages = getOpUsagesByDevice(ops, deviceId);
|
||
|
|
||
|
ArrayList<String> usedPermGroups = new ArrayList<>(rawUsages.keySet());
|
||
|
|
||
|
// If we have a phone call, and a carrier privileged app using microphone, hide the
|
||
|
// phone call.
|
||
|
AudioManager audioManager = mContext.getSystemService(AudioManager.class);
|
||
|
boolean hasPhoneCall = usedPermGroups.contains(OPSTR_PHONE_CALL_CAMERA)
|
||
|
|| usedPermGroups.contains(OPSTR_PHONE_CALL_MICROPHONE);
|
||
|
if (hasPhoneCall && usedPermGroups.contains(MICROPHONE) && audioManager.getMode()
|
||
|
== MODE_IN_COMMUNICATION) {
|
||
|
TelephonyManager telephonyManager =
|
||
|
mContext.getSystemService(TelephonyManager.class);
|
||
|
List<OpUsage> permUsages = rawUsages.get(MICROPHONE);
|
||
|
for (int usageNum = 0; usageNum < permUsages.size(); usageNum++) {
|
||
|
if (telephonyManager.checkCarrierPrivilegesForPackage(
|
||
|
permUsages.get(usageNum).packageName)
|
||
|
== CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
|
||
|
usedPermGroups.remove(OPSTR_PHONE_CALL_CAMERA);
|
||
|
usedPermGroups.remove(OPSTR_PHONE_CALL_MICROPHONE);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// map of package name -> map of attribution tag -> attribution labels
|
||
|
ArrayMap<String, Map<String, String>> subAttributionLabelsMap = new ArrayMap<>();
|
||
|
|
||
|
for (int permGroupNum = 0; permGroupNum < usedPermGroups.size(); permGroupNum++) {
|
||
|
boolean isPhone = false;
|
||
|
String permGroup = usedPermGroups.get(permGroupNum);
|
||
|
|
||
|
ArrayMap<OpUsage, CharSequence> usagesWithLabels =
|
||
|
getUniqueUsagesWithLabels(permGroup, rawUsages.get(permGroup));
|
||
|
|
||
|
updateSubattributionLabelsMap(rawUsages.get(permGroup), subAttributionLabelsMap);
|
||
|
|
||
|
if (permGroup.equals(OPSTR_PHONE_CALL_MICROPHONE)) {
|
||
|
isPhone = true;
|
||
|
permGroup = MICROPHONE;
|
||
|
} else if (permGroup.equals(OPSTR_PHONE_CALL_CAMERA)) {
|
||
|
isPhone = true;
|
||
|
permGroup = CAMERA;
|
||
|
}
|
||
|
|
||
|
for (int usageNum = 0; usageNum < usagesWithLabels.size(); usageNum++) {
|
||
|
OpUsage usage = usagesWithLabels.keyAt(usageNum);
|
||
|
String attributionLabel = subAttributionLabelsMap.getOrDefault(usage.packageName,
|
||
|
new ArrayMap<>()).getOrDefault(usage.attributionTag, null);
|
||
|
usages.add(
|
||
|
new PermissionGroupUsage(usage.packageName, usage.uid, usage.lastAccessTime,
|
||
|
permGroup,
|
||
|
usage.isRunning, isPhone, usage.attributionTag, attributionLabel,
|
||
|
usagesWithLabels.valueAt(usageNum), deviceId));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return usages;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return Op usage for CAMERA, LOCATION AND MICROPHONE for all packages and all connected
|
||
|
* devices.
|
||
|
* The returned data is to power privacy indicator.
|
||
|
*/
|
||
|
public @NonNull List<PermissionGroupUsage> getOpUsageDataForAllDevices(
|
||
|
boolean includeMicrophoneUsage) {
|
||
|
List<PermissionGroupUsage> allUsages = new ArrayList<>();
|
||
|
List<VirtualDevice> virtualDevices = mVirtualDeviceManager.getVirtualDevices();
|
||
|
ArraySet<String> persistentDeviceIds = new ArraySet<>();
|
||
|
|
||
|
for (int num = 0; num < virtualDevices.size(); num++) {
|
||
|
persistentDeviceIds.add(virtualDevices.get(num).getPersistentDeviceId());
|
||
|
}
|
||
|
persistentDeviceIds.add(VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
|
||
|
|
||
|
for (int index = 0; index < persistentDeviceIds.size(); index++) {
|
||
|
allUsages.addAll(
|
||
|
getOpUsageDataByDevice(includeMicrophoneUsage,
|
||
|
persistentDeviceIds.valueAt(index)));
|
||
|
}
|
||
|
|
||
|
return allUsages;
|
||
|
}
|
||
|
|
||
|
|
||
|
private void updateSubattributionLabelsMap(List<OpUsage> usages,
|
||
|
ArrayMap<String, Map<String, String>> subAttributionLabelsMap) {
|
||
|
if (usages == null || usages.isEmpty()) {
|
||
|
return;
|
||
|
}
|
||
|
for (OpUsage usage : usages) {
|
||
|
if (usage.attributionTag != null && !subAttributionLabelsMap.containsKey(
|
||
|
usage.packageName)) {
|
||
|
subAttributionLabelsMap.put(usage.packageName,
|
||
|
getSubattributionLabelsForPackage(usage.packageName, usage.uid));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Query attribution labels for a package
|
||
|
*
|
||
|
* @param packageName
|
||
|
* @param uid
|
||
|
* @return map of attribution tag -> attribution labels for a package
|
||
|
*/
|
||
|
private ArrayMap<String, String> getSubattributionLabelsForPackage(String packageName,
|
||
|
int uid) {
|
||
|
ArrayMap<String, String> attributionLabelMap = new ArrayMap<>();
|
||
|
UserHandle user = UserHandle.getUserHandleForUid(uid);
|
||
|
try {
|
||
|
if (!isSubattributionSupported(packageName, uid)) {
|
||
|
return attributionLabelMap;
|
||
|
}
|
||
|
Context userContext = getUserContext(user);
|
||
|
PackageInfo packageInfo = userContext.getPackageManager().getPackageInfo(
|
||
|
packageName,
|
||
|
PackageManager.PackageInfoFlags.of(
|
||
|
PackageManager.GET_PERMISSIONS | PackageManager.GET_ATTRIBUTIONS_LONG));
|
||
|
Context pkgContext = userContext.createPackageContext(packageInfo.packageName, 0);
|
||
|
for (Attribution attribution : packageInfo.attributions) {
|
||
|
try {
|
||
|
String resourceForLabel = pkgContext.getString(attribution.getLabel());
|
||
|
attributionLabelMap.put(attribution.getTag(), resourceForLabel);
|
||
|
} catch (Resources.NotFoundException e) {
|
||
|
// Shouldn't happen, do nothing
|
||
|
}
|
||
|
}
|
||
|
} catch (PackageManager.NameNotFoundException e) {
|
||
|
// Did not find the package, do nothing
|
||
|
}
|
||
|
return attributionLabelMap;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if the app satisfies subattribution policies and supports it
|
||
|
*/
|
||
|
private boolean isSubattributionSupported(String packageName, int uid) {
|
||
|
try {
|
||
|
if (!isLocationProvider(packageName)) {
|
||
|
return false;
|
||
|
}
|
||
|
PackageManager userPkgManager =
|
||
|
getUserContext(UserHandle.getUserHandleForUid(uid)).getPackageManager();
|
||
|
ApplicationInfo appInfo = userPkgManager.getApplicationInfoAsUser(packageName,
|
||
|
PackageManager.ApplicationInfoFlags.of(0),
|
||
|
UserHandle.getUserId(uid));
|
||
|
if (appInfo != null) {
|
||
|
return appInfo.areAttributionsUserVisible();
|
||
|
}
|
||
|
return false;
|
||
|
} catch (PackageManager.NameNotFoundException e) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param packageName
|
||
|
* @return If the package is location provider
|
||
|
*/
|
||
|
private boolean isLocationProvider(String packageName) {
|
||
|
return Objects.requireNonNull(
|
||
|
mContext.getSystemService(LocationManager.class)).isProviderPackage(packageName);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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
|
||
|
* @param deviceId which device to get op usage for
|
||
|
* @return A map of permission group -> list of usages that are recent or running
|
||
|
*/
|
||
|
private Map<String, List<OpUsage>> getOpUsagesByDevice(List<String> opNames, String deviceId) {
|
||
|
List<AppOpsManager.PackageOps> ops;
|
||
|
try {
|
||
|
if (Flags.deviceAwarePermissionApisEnabled()) {
|
||
|
ops = mAppOpsManager.getPackagesForOps(opNames.toArray(new String[opNames.size()]),
|
||
|
deviceId);
|
||
|
} else if (!Objects.equals(deviceId,
|
||
|
VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) {
|
||
|
Slog.w(LOG_TAG,
|
||
|
"device_aware_permission_apis_enabled flag not enabled when deviceId is "
|
||
|
+ "not default");
|
||
|
return Collections.emptyMap();
|
||
|
} else {
|
||
|
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<Integer, 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 (attrOpEntry.isRunning()) {
|
||
|
lastAccessTime = now;
|
||
|
}
|
||
|
|
||
|
if (lastAccessTime < recentThreshold && !attrOpEntry.isRunning()) {
|
||
|
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(),
|
||
|
op, proxy.getUid(), lastAccessTime, isRunning, null);
|
||
|
}
|
||
|
|
||
|
String permGroupName = getGroupForOp(op);
|
||
|
OpUsage usage = new OpUsage(packageName, attributionTag, op, uid,
|
||
|
lastAccessTime, isRunning, proxyUsage);
|
||
|
|
||
|
Integer packageAttr = usage.getPackageIdHash();
|
||
|
if (!usages.containsKey(permGroupName)) {
|
||
|
ArrayMap<Integer, OpUsage> map = new ArrayMap<>();
|
||
|
map.put(packageAttr, usage);
|
||
|
usages.put(permGroupName, map);
|
||
|
} else {
|
||
|
Map<Integer, 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;
|
||
|
}
|
||
|
|
||
|
private CharSequence formatLabelList(List<CharSequence> labels) {
|
||
|
return ListFormatter.getInstance().format(labels);
|
||
|
}
|
||
|
|
||
|
private ArrayMap<OpUsage, CharSequence> getUniqueUsagesWithLabels(String permGroup,
|
||
|
List<OpUsage> usages) {
|
||
|
ArrayMap<OpUsage, CharSequence> usagesAndLabels = new ArrayMap<>();
|
||
|
|
||
|
if (usages == null || usages.isEmpty()) {
|
||
|
return usagesAndLabels;
|
||
|
}
|
||
|
|
||
|
ArrayMap<Integer, OpUsage> allUsages = new ArrayMap<>();
|
||
|
// map of packageName and uid hash -> most recent non-proxy-related usage for that uid.
|
||
|
ArrayMap<Integer, OpUsage> mostRecentUsages = new ArrayMap<>();
|
||
|
// set of all packages involved in a proxy usage
|
||
|
ArraySet<Integer> proxyPackages = new ArraySet<>();
|
||
|
// map of usage -> list of proxy app labels
|
||
|
ArrayMap<OpUsage, ArrayList<CharSequence>> proxyLabels = new ArrayMap<>();
|
||
|
// map of usage.proxy hash -> usage hash, telling us if a usage is a proxy
|
||
|
ArrayMap<Integer, OpUsage> proxies = new ArrayMap<>();
|
||
|
|
||
|
for (int i = 0; i < usages.size(); i++) {
|
||
|
OpUsage usage = usages.get(i);
|
||
|
allUsages.put(usage.getPackageIdHash(), usage);
|
||
|
if (usage.proxy != null) {
|
||
|
proxies.put(usage.proxy.getPackageIdHash(), usage);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// find all possible end points for chains, and find the most recent of the rest of the uses
|
||
|
for (int usageNum = 0; usageNum < usages.size(); usageNum++) {
|
||
|
OpUsage usage = usages.get(usageNum);
|
||
|
if (usage == null) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
int usageAttr = usage.getPackageIdHash();
|
||
|
// If this usage has a proxy, but is not a proxy, it is the end of a chain.
|
||
|
// TODO remove once camera converted
|
||
|
if (!proxies.containsKey(usageAttr) && usage.proxy != null
|
||
|
&& !MICROPHONE.equals(permGroup)) {
|
||
|
proxyLabels.put(usage, new ArrayList<>());
|
||
|
proxyPackages.add(usage.getPackageIdHash());
|
||
|
}
|
||
|
// If this usage is not by the system, and is more recent than the next-most recent
|
||
|
// for it's uid and package name, save it.
|
||
|
int usageId = usage.getPackageIdHash();
|
||
|
OpUsage lastMostRecent = mostRecentUsages.get(usageId);
|
||
|
if (shouldShowPackage(usage.packageName) && (lastMostRecent == null
|
||
|
|| usage.lastAccessTime > lastMostRecent.lastAccessTime)) {
|
||
|
mostRecentUsages.put(usageId, usage);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// get all the proxy labels
|
||
|
for (int numStart = 0; numStart < proxyLabels.size(); numStart++) {
|
||
|
OpUsage start = proxyLabels.keyAt(numStart);
|
||
|
// Remove any non-proxy usage for the starting package
|
||
|
mostRecentUsages.remove(start.getPackageIdHash());
|
||
|
OpUsage currentUsage = proxyLabels.keyAt(numStart);
|
||
|
ArrayList<CharSequence> proxyLabelList = proxyLabels.get(currentUsage);
|
||
|
if (currentUsage == null || proxyLabelList == null) {
|
||
|
continue;
|
||
|
}
|
||
|
int iterNum = 0;
|
||
|
int maxUsages = allUsages.size();
|
||
|
while (currentUsage.proxy != null) {
|
||
|
|
||
|
if (allUsages.containsKey(currentUsage.proxy.getPackageIdHash())) {
|
||
|
currentUsage = allUsages.get(currentUsage.proxy.getPackageIdHash());
|
||
|
} else {
|
||
|
// We are missing the proxy usage. This may be because it's a one-step trusted
|
||
|
// proxy. Check if we should show the proxy label, and show it, if so.
|
||
|
OpUsage proxy = currentUsage.proxy;
|
||
|
if (shouldShowPackage(proxy.packageName)) {
|
||
|
currentUsage = proxy;
|
||
|
// We've effectively added one usage, so increment the max number of usages
|
||
|
maxUsages++;
|
||
|
} else {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (currentUsage == null || iterNum == maxUsages
|
||
|
|| currentUsage.getPackageIdHash() == start.getPackageIdHash()) {
|
||
|
// We have an invalid state, or a cycle, so break
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
proxyPackages.add(currentUsage.getPackageIdHash());
|
||
|
// Don't add an app label for the main app, or the system app
|
||
|
if (!currentUsage.packageName.equals(start.packageName)
|
||
|
&& shouldShowPackage(currentUsage.packageName)) {
|
||
|
try {
|
||
|
PackageManager userPkgManager =
|
||
|
getUserContext(currentUsage.getUser()).getPackageManager();
|
||
|
ApplicationInfo appInfo = userPkgManager.getApplicationInfo(
|
||
|
currentUsage.packageName, 0);
|
||
|
CharSequence appLabel = appInfo.loadLabel(userPkgManager);
|
||
|
// If we don't already have the app label add it
|
||
|
if (!proxyLabelList.contains(appLabel)) {
|
||
|
proxyLabelList.add(appLabel);
|
||
|
}
|
||
|
} catch (PackageManager.NameNotFoundException e) {
|
||
|
// Ignore
|
||
|
}
|
||
|
}
|
||
|
iterNum++;
|
||
|
}
|
||
|
|
||
|
// TODO ntmyren: remove this proxy logic once camera is converted to AttributionSource
|
||
|
// For now: don't add mic proxy usages
|
||
|
if (!MICROPHONE.equals(permGroup)) {
|
||
|
usagesAndLabels.put(start,
|
||
|
proxyLabelList.isEmpty() ? null : formatLabelList(proxyLabelList));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
synchronized (mAttributionChains) {
|
||
|
for (int i = 0; i < mAttributionChains.size(); i++) {
|
||
|
List<AccessChainLink> usageList = mAttributionChains.valueAt(i);
|
||
|
int lastVisible = usageList.size() - 1;
|
||
|
// TODO ntmyren: remove this mic code once camera is converted to AttributionSource
|
||
|
// if the list is empty or incomplete, do not show it.
|
||
|
if (usageList.isEmpty() || !usageList.get(lastVisible).isEnd()
|
||
|
|| !usageList.get(0).isStart()
|
||
|
|| !permGroup.equals(getGroupForOp(usageList.get(0).usage.op))
|
||
|
|| !MICROPHONE.equals(permGroup)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
//TODO ntmyren: remove once camera etc. etc.
|
||
|
for (AccessChainLink link : usageList) {
|
||
|
proxyPackages.add(link.usage.getPackageIdHash());
|
||
|
}
|
||
|
|
||
|
AccessChainLink start = usageList.get(0);
|
||
|
AccessChainLink lastVisibleLink = usageList.get(lastVisible);
|
||
|
while (lastVisible > 0 && !shouldShowPackage(lastVisibleLink.usage.packageName)) {
|
||
|
lastVisible--;
|
||
|
lastVisibleLink = usageList.get(lastVisible);
|
||
|
}
|
||
|
String proxyLabel = null;
|
||
|
if (!lastVisibleLink.usage.packageName.equals(start.usage.packageName)) {
|
||
|
try {
|
||
|
PackageManager userPkgManager =
|
||
|
getUserContext(lastVisibleLink.usage.getUser()).getPackageManager();
|
||
|
ApplicationInfo appInfo = userPkgManager.getApplicationInfo(
|
||
|
lastVisibleLink.usage.packageName, 0);
|
||
|
proxyLabel = appInfo.loadLabel(userPkgManager).toString();
|
||
|
} catch (PackageManager.NameNotFoundException e) {
|
||
|
// do nothing
|
||
|
}
|
||
|
}
|
||
|
usagesAndLabels.put(start.usage, proxyLabel);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for (int packageHash : mostRecentUsages.keySet()) {
|
||
|
if (!proxyPackages.contains(packageHash)) {
|
||
|
usagesAndLabels.put(mostRecentUsages.get(packageHash), null);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return usagesAndLabels;
|
||
|
}
|
||
|
|
||
|
private boolean shouldShowPackage(String packageName) {
|
||
|
return PermissionManager.shouldShowPackageForIndicatorCached(mContext, 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 String op;
|
||
|
public final int uid;
|
||
|
public final long lastAccessTime;
|
||
|
public final OpUsage proxy;
|
||
|
public final boolean isRunning;
|
||
|
|
||
|
OpUsage(String packageName, String attributionTag, String op, int uid, long lastAccessTime,
|
||
|
boolean isRunning, OpUsage proxy) {
|
||
|
this.packageName = packageName;
|
||
|
this.attributionTag = attributionTag;
|
||
|
this.op = op;
|
||
|
this.uid = uid;
|
||
|
this.lastAccessTime = lastAccessTime;
|
||
|
this.isRunning = isRunning;
|
||
|
this.proxy = proxy;
|
||
|
}
|
||
|
|
||
|
public UserHandle getUser() {
|
||
|
return UserHandle.getUserHandleForUid(uid);
|
||
|
}
|
||
|
|
||
|
public int getPackageIdHash() {
|
||
|
return Objects.hash(packageName, uid);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int hashCode() {
|
||
|
return Objects.hash(packageName, attributionTag, op, uid, lastAccessTime, isRunning);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean equals(Object obj) {
|
||
|
if (!(obj instanceof OpUsage)) {
|
||
|
return false;
|
||
|
}
|
||
|
OpUsage other = (OpUsage) obj;
|
||
|
return Objects.equals(packageName, other.packageName) && Objects.equals(attributionTag,
|
||
|
other.attributionTag) && Objects.equals(op, other.op) && uid == other.uid
|
||
|
&& lastAccessTime == other.lastAccessTime && isRunning == other.isRunning;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class AccessChainLink {
|
||
|
public final OpUsage usage;
|
||
|
public final @AttributionFlags int flags;
|
||
|
|
||
|
AccessChainLink(String op, String packageName, String attributionTag, int uid,
|
||
|
int flags) {
|
||
|
this.usage = new OpUsage(packageName, attributionTag, op, uid,
|
||
|
System.currentTimeMillis(), true, null);
|
||
|
this.flags = flags;
|
||
|
}
|
||
|
|
||
|
public boolean isEnd() {
|
||
|
return (flags & ATTRIBUTION_FLAG_ACCESSOR) != 0;
|
||
|
}
|
||
|
|
||
|
public boolean isStart() {
|
||
|
return (flags & ATTRIBUTION_FLAG_RECEIVER) != 0;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean equals(Object obj) {
|
||
|
if (!(obj instanceof AccessChainLink)) {
|
||
|
return false;
|
||
|
}
|
||
|
AccessChainLink other = (AccessChainLink) obj;
|
||
|
return other.flags == flags && packageAndOpEquals(other.usage.op,
|
||
|
other.usage.packageName, other.usage.attributionTag, other.usage.uid);
|
||
|
}
|
||
|
|
||
|
public boolean packageAndOpEquals(String op, String packageName, String attributionTag,
|
||
|
int uid) {
|
||
|
return Objects.equals(op, usage.op) && Objects.equals(packageName, usage.packageName)
|
||
|
&& Objects.equals(attributionTag, usage.attributionTag) && uid == usage.uid;
|
||
|
}
|
||
|
}
|
||
|
}
|