/* * 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 LOCATION_OPS = List.of( OPSTR_COARSE_LOCATION, OPSTR_FINE_LOCATION ); private static final List MIC_OPS = List.of( OPSTR_PHONE_CALL_MICROPHONE, OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO, OPSTR_RECORD_AUDIO ); private static final List 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 mUserContexts; private PackageManager mPkgManager; private AppOpsManager mAppOpsManager; private VirtualDeviceManager mVirtualDeviceManager; @GuardedBy("mAttributionChains") private final ArrayMap> 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 toRemove = new ArrayList<>(); for (int i = 0; i < numChains; i++) { int chainId = mAttributionChains.keyAt(i); ArrayList 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 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 getOpUsageDataByDevice( boolean includeMicrophoneUsage, String deviceId) { List usages = new ArrayList<>(); if (!shouldShowIndicators()) { return usages; } List ops = new ArrayList<>(CAMERA_OPS); if (shouldShowLocationIndicator()) { ops.addAll(LOCATION_OPS); } if (includeMicrophoneUsage) { ops.addAll(MIC_OPS); } Map> rawUsages = getOpUsagesByDevice(ops, deviceId); ArrayList 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 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> subAttributionLabelsMap = new ArrayMap<>(); for (int permGroupNum = 0; permGroupNum < usedPermGroups.size(); permGroupNum++) { boolean isPhone = false; String permGroup = usedPermGroups.get(permGroupNum); ArrayMap 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 getOpUsageDataForAllDevices( boolean includeMicrophoneUsage) { List allUsages = new ArrayList<>(); List virtualDevices = mVirtualDeviceManager.getVirtualDevices(); ArraySet 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 usages, ArrayMap> 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 getSubattributionLabelsForPackage(String packageName, int uid) { ArrayMap 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> getOpUsagesByDevice(List opNames, String deviceId) { List 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> 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 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 map = new ArrayMap<>(); map.put(packageAttr, usage); usages.put(permGroupName, map); } else { Map 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> flattenedUsages = new ArrayMap<>(); List 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 labels) { return ListFormatter.getInstance().format(labels); } private ArrayMap getUniqueUsagesWithLabels(String permGroup, List usages) { ArrayMap usagesAndLabels = new ArrayMap<>(); if (usages == null || usages.isEmpty()) { return usagesAndLabels; } ArrayMap allUsages = new ArrayMap<>(); // map of packageName and uid hash -> most recent non-proxy-related usage for that uid. ArrayMap mostRecentUsages = new ArrayMap<>(); // set of all packages involved in a proxy usage ArraySet proxyPackages = new ArraySet<>(); // map of usage -> list of proxy app labels ArrayMap> proxyLabels = new ArrayMap<>(); // map of usage.proxy hash -> usage hash, telling us if a usage is a proxy ArrayMap 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 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 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; } } }