1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.permission; 18 19 import static android.Manifest.permission_group.CAMERA; 20 import static android.Manifest.permission_group.LOCATION; 21 import static android.Manifest.permission_group.MICROPHONE; 22 import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE; 23 import static android.app.AppOpsManager.ATTRIBUTION_FLAGS_NONE; 24 import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR; 25 import static android.app.AppOpsManager.ATTRIBUTION_FLAG_RECEIVER; 26 import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED; 27 import static android.app.AppOpsManager.AttributionFlags; 28 import static android.app.AppOpsManager.OPSTR_CAMERA; 29 import static android.app.AppOpsManager.OPSTR_COARSE_LOCATION; 30 import static android.app.AppOpsManager.OPSTR_FINE_LOCATION; 31 import static android.app.AppOpsManager.OPSTR_PHONE_CALL_CAMERA; 32 import static android.app.AppOpsManager.OPSTR_PHONE_CALL_MICROPHONE; 33 import static android.app.AppOpsManager.OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO; 34 import static android.app.AppOpsManager.OPSTR_RECORD_AUDIO; 35 import static android.app.AppOpsManager.OP_CAMERA; 36 import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED; 37 import static android.app.AppOpsManager.OP_RECORD_AUDIO; 38 import static android.media.AudioSystem.MODE_IN_COMMUNICATION; 39 import static android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS; 40 41 import android.annotation.NonNull; 42 import android.annotation.Nullable; 43 import android.app.AppOpsManager; 44 import android.content.Context; 45 import android.content.pm.ApplicationInfo; 46 import android.content.pm.Attribution; 47 import android.content.pm.PackageInfo; 48 import android.content.pm.PackageManager; 49 import android.content.res.Resources; 50 import android.icu.text.ListFormatter; 51 import android.location.LocationManager; 52 import android.media.AudioManager; 53 import android.os.Process; 54 import android.os.UserHandle; 55 import android.provider.DeviceConfig; 56 import android.telephony.TelephonyManager; 57 import android.util.ArrayMap; 58 import android.util.ArraySet; 59 60 import com.android.internal.annotations.GuardedBy; 61 62 import java.util.ArrayList; 63 import java.util.Collections; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.Objects; 67 68 /** 69 * A helper which gets all apps which have used microphone, camera, and possible location 70 * permissions within a certain timeframe, as well as possible special attributions, and if the 71 * usage is a phone call. 72 * 73 * @hide 74 */ 75 public class PermissionUsageHelper implements AppOpsManager.OnOpActiveChangedListener, 76 AppOpsManager.OnOpStartedListener { 77 78 /** 79 * Whether to show the mic and camera icons. 80 */ 81 private static final String PROPERTY_CAMERA_MIC_ICONS_ENABLED = "camera_mic_icons_enabled"; 82 83 /** 84 * Whether to show the location indicators. 85 */ 86 private static final String PROPERTY_LOCATION_INDICATORS_ENABLED = 87 "location_indicators_enabled"; 88 89 /** 90 * Whether to show the Permissions Hub. 91 */ 92 private static final String PROPERTY_PERMISSIONS_HUB_2_ENABLED = "permissions_hub_2_enabled"; 93 94 /** 95 * How long after an access to show it as "recent" 96 */ 97 private static final String RECENT_ACCESS_TIME_MS = "recent_access_time_ms"; 98 99 /** 100 * How long after an access to show it as "running" 101 */ 102 private static final String RUNNING_ACCESS_TIME_MS = "running_access_time_ms"; 103 104 private static final String SYSTEM_PKG = "android"; 105 106 private static final long DEFAULT_RUNNING_TIME_MS = 5000L; 107 private static final long DEFAULT_RECENT_TIME_MS = 15000L; 108 shouldShowPermissionsHub()109 private static boolean shouldShowPermissionsHub() { 110 return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, 111 PROPERTY_PERMISSIONS_HUB_2_ENABLED, false); 112 } 113 shouldShowIndicators()114 private static boolean shouldShowIndicators() { 115 return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, 116 PROPERTY_CAMERA_MIC_ICONS_ENABLED, true) || shouldShowPermissionsHub(); 117 } 118 shouldShowLocationIndicator()119 private static boolean shouldShowLocationIndicator() { 120 return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, 121 PROPERTY_LOCATION_INDICATORS_ENABLED, false); 122 } 123 getRecentThreshold(Long now)124 private static long getRecentThreshold(Long now) { 125 return now - DeviceConfig.getLong(DeviceConfig.NAMESPACE_PRIVACY, 126 RECENT_ACCESS_TIME_MS, DEFAULT_RECENT_TIME_MS); 127 } 128 getRunningThreshold(Long now)129 private static long getRunningThreshold(Long now) { 130 return now - DeviceConfig.getLong(DeviceConfig.NAMESPACE_PRIVACY, 131 RUNNING_ACCESS_TIME_MS, DEFAULT_RUNNING_TIME_MS); 132 } 133 134 private static final List<String> LOCATION_OPS = List.of( 135 OPSTR_COARSE_LOCATION, 136 OPSTR_FINE_LOCATION 137 ); 138 139 private static final List<String> MIC_OPS = List.of( 140 OPSTR_PHONE_CALL_MICROPHONE, 141 OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO, 142 OPSTR_RECORD_AUDIO 143 ); 144 145 private static final List<String> CAMERA_OPS = List.of( 146 OPSTR_PHONE_CALL_CAMERA, 147 OPSTR_CAMERA 148 ); 149 getGroupForOp(String op)150 private static @NonNull String getGroupForOp(String op) { 151 switch (op) { 152 case OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO: 153 case OPSTR_RECORD_AUDIO: 154 return MICROPHONE; 155 case OPSTR_CAMERA: 156 return CAMERA; 157 case OPSTR_PHONE_CALL_MICROPHONE: 158 case OPSTR_PHONE_CALL_CAMERA: 159 return op; 160 case OPSTR_COARSE_LOCATION: 161 case OPSTR_FINE_LOCATION: 162 return LOCATION; 163 default: 164 throw new IllegalArgumentException("Unknown app op: " + op); 165 } 166 } 167 168 private Context mContext; 169 private ArrayMap<UserHandle, Context> mUserContexts; 170 private PackageManager mPkgManager; 171 private AppOpsManager mAppOpsManager; 172 @GuardedBy("mAttributionChains") 173 private final ArrayMap<Integer, ArrayList<AccessChainLink>> mAttributionChains = 174 new ArrayMap<>(); 175 176 /** 177 * Constructor for PermissionUsageHelper 178 * 179 * @param context The context from which to derive the package information 180 */ PermissionUsageHelper(@onNull Context context)181 public PermissionUsageHelper(@NonNull Context context) { 182 mContext = context; 183 mPkgManager = context.getPackageManager(); 184 mAppOpsManager = context.getSystemService(AppOpsManager.class); 185 mUserContexts = new ArrayMap<>(); 186 mUserContexts.put(Process.myUserHandle(), mContext); 187 // TODO ntmyren: make this listen for flag enable/disable changes 188 String[] opStrs = {OPSTR_CAMERA, OPSTR_RECORD_AUDIO}; 189 mAppOpsManager.startWatchingActive(opStrs, context.getMainExecutor(), this); 190 int[] ops = {OP_CAMERA, OP_RECORD_AUDIO}; 191 mAppOpsManager.startWatchingStarted(ops, this); 192 } 193 getUserContext(UserHandle user)194 private Context getUserContext(UserHandle user) { 195 if (!(mUserContexts.containsKey(user))) { 196 mUserContexts.put(user, mContext.createContextAsUser(user, 0)); 197 } 198 return mUserContexts.get(user); 199 } 200 tearDown()201 public void tearDown() { 202 mAppOpsManager.stopWatchingActive(this); 203 mAppOpsManager.stopWatchingStarted(this); 204 } 205 206 @Override onOpActiveChanged(@onNull String op, int uid, @NonNull String packageName, boolean active)207 public void onOpActiveChanged(@NonNull String op, int uid, @NonNull String packageName, 208 boolean active) { 209 // not part of an attribution chain. Do nothing 210 } 211 212 @Override onOpActiveChanged(@onNull String op, int uid, @NonNull String packageName, @Nullable String attributionTag, boolean active, @AttributionFlags int attributionFlags, int attributionChainId)213 public void onOpActiveChanged(@NonNull String op, int uid, @NonNull String packageName, 214 @Nullable String attributionTag, boolean active, @AttributionFlags int attributionFlags, 215 int attributionChainId) { 216 if (active) { 217 // Started callback handles these 218 return; 219 } 220 221 // if any link in the chain is finished, remove the chain. Then, find any other chains that 222 // contain this op/package/uid/tag combination, and remove them, as well. 223 // TODO ntmyren: be smarter about this 224 synchronized (mAttributionChains) { 225 mAttributionChains.remove(attributionChainId); 226 int numChains = mAttributionChains.size(); 227 ArrayList<Integer> toRemove = new ArrayList<>(); 228 for (int i = 0; i < numChains; i++) { 229 int chainId = mAttributionChains.keyAt(i); 230 ArrayList<AccessChainLink> chain = mAttributionChains.valueAt(i); 231 int chainSize = chain.size(); 232 for (int j = 0; j < chainSize; j++) { 233 AccessChainLink link = chain.get(j); 234 if (link.packageAndOpEquals(op, packageName, attributionTag, uid)) { 235 toRemove.add(chainId); 236 break; 237 } 238 } 239 } 240 mAttributionChains.removeAll(toRemove); 241 } 242 } 243 244 @Override onOpStarted(int op, int uid, String packageName, String attributionTag, @AppOpsManager.OpFlags int flags, @AppOpsManager.Mode int result)245 public void onOpStarted(int op, int uid, String packageName, String attributionTag, 246 @AppOpsManager.OpFlags int flags, @AppOpsManager.Mode int result) { 247 // not part of an attribution chain. Do nothing 248 } 249 250 @Override 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)251 public void onOpStarted(int op, int uid, String packageName, String attributionTag, 252 @AppOpsManager.OpFlags int flags, @AppOpsManager.Mode int result, 253 @StartedType int startedType, @AttributionFlags int attributionFlags, 254 int attributionChainId) { 255 if (startedType == START_TYPE_FAILED || attributionChainId == ATTRIBUTION_CHAIN_ID_NONE 256 || attributionFlags == ATTRIBUTION_FLAGS_NONE 257 || (attributionFlags & ATTRIBUTION_FLAG_TRUSTED) == 0) { 258 // If this is not a successful start, or it is not a chain, or it is untrusted, return 259 return; 260 } 261 synchronized (mAttributionChains) { 262 addLinkToChainIfNotPresentLocked(AppOpsManager.opToPublicName(op), packageName, uid, 263 attributionTag, attributionFlags, attributionChainId); 264 } 265 } 266 addLinkToChainIfNotPresentLocked(String op, String packageName, int uid, String attributionTag, int attributionFlags, int attributionChainId)267 private void addLinkToChainIfNotPresentLocked(String op, String packageName, int uid, 268 String attributionTag, int attributionFlags, int attributionChainId) { 269 270 ArrayList<AccessChainLink> currentChain = mAttributionChains.computeIfAbsent( 271 attributionChainId, k -> new ArrayList<>()); 272 AccessChainLink link = new AccessChainLink(op, packageName, attributionTag, uid, 273 attributionFlags); 274 275 if (currentChain.contains(link)) { 276 return; 277 } 278 279 int currSize = currentChain.size(); 280 if (currSize == 0 || link.isEnd() || !currentChain.get(currSize - 1).isEnd()) { 281 // if the list is empty, this link is the end, or the last link in the current chain 282 // isn't the end, add it to the end 283 currentChain.add(link); 284 } else if (link.isStart()) { 285 currentChain.add(0, link); 286 } else if (currentChain.get(currentChain.size() - 1).isEnd()) { 287 // we already have the end, and this is a mid node, so insert before the end 288 currentChain.add(currSize - 1, link); 289 } 290 } 291 292 /** 293 * @see PermissionManager.getIndicatorAppOpUsageData 294 */ getOpUsageData(boolean isMicMuted)295 public @NonNull List<PermissionGroupUsage> getOpUsageData(boolean isMicMuted) { 296 List<PermissionGroupUsage> usages = new ArrayList<>(); 297 298 if (!shouldShowIndicators()) { 299 return usages; 300 } 301 302 List<String> ops = new ArrayList<>(CAMERA_OPS); 303 if (shouldShowLocationIndicator()) { 304 ops.addAll(LOCATION_OPS); 305 } 306 if (!isMicMuted) { 307 ops.addAll(MIC_OPS); 308 } 309 310 Map<String, List<OpUsage>> rawUsages = getOpUsages(ops); 311 312 ArrayList<String> usedPermGroups = new ArrayList<>(rawUsages.keySet()); 313 314 // If we have a phone call, and a carrier privileged app using microphone, hide the 315 // phone call. 316 AudioManager audioManager = mContext.getSystemService(AudioManager.class); 317 boolean hasPhoneCall = usedPermGroups.contains(OPSTR_PHONE_CALL_CAMERA) 318 || usedPermGroups.contains(OPSTR_PHONE_CALL_MICROPHONE); 319 if (hasPhoneCall && usedPermGroups.contains(MICROPHONE) && audioManager.getMode() 320 == MODE_IN_COMMUNICATION) { 321 TelephonyManager telephonyManager = 322 mContext.getSystemService(TelephonyManager.class); 323 List<OpUsage> permUsages = rawUsages.get(MICROPHONE); 324 for (int usageNum = 0; usageNum < permUsages.size(); usageNum++) { 325 if (telephonyManager.checkCarrierPrivilegesForPackage( 326 permUsages.get(usageNum).packageName) 327 == CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) { 328 usedPermGroups.remove(OPSTR_PHONE_CALL_CAMERA); 329 usedPermGroups.remove(OPSTR_PHONE_CALL_MICROPHONE); 330 } 331 } 332 } 333 334 // map of package name -> map of attribution tag -> attribution labels 335 ArrayMap<String, Map<String, String>> subAttributionLabelsMap = new ArrayMap<>(); 336 337 for (int permGroupNum = 0; permGroupNum < usedPermGroups.size(); permGroupNum++) { 338 boolean isPhone = false; 339 String permGroup = usedPermGroups.get(permGroupNum); 340 341 ArrayMap<OpUsage, CharSequence> usagesWithLabels = 342 getUniqueUsagesWithLabels(permGroup, rawUsages.get(permGroup)); 343 344 updateSubattributionLabelsMap(rawUsages.get(permGroup), subAttributionLabelsMap); 345 346 if (permGroup.equals(OPSTR_PHONE_CALL_MICROPHONE)) { 347 isPhone = true; 348 permGroup = MICROPHONE; 349 } else if (permGroup.equals(OPSTR_PHONE_CALL_CAMERA)) { 350 isPhone = true; 351 permGroup = CAMERA; 352 } 353 354 for (int usageNum = 0; usageNum < usagesWithLabels.size(); usageNum++) { 355 OpUsage usage = usagesWithLabels.keyAt(usageNum); 356 String attributionLabel = subAttributionLabelsMap.getOrDefault(usage.packageName, 357 new ArrayMap<>()).getOrDefault(usage.attributionTag, null); 358 usages.add( 359 new PermissionGroupUsage(usage.packageName, usage.uid, usage.lastAccessTime, 360 permGroup, 361 usage.isRunning, isPhone, usage.attributionTag, attributionLabel, 362 usagesWithLabels.valueAt(usageNum))); 363 } 364 } 365 366 return usages; 367 } 368 updateSubattributionLabelsMap(List<OpUsage> usages, ArrayMap<String, Map<String, String>> subAttributionLabelsMap)369 private void updateSubattributionLabelsMap(List<OpUsage> usages, 370 ArrayMap<String, Map<String, String>> subAttributionLabelsMap) { 371 if (usages == null || usages.isEmpty()) { 372 return; 373 } 374 for (OpUsage usage : usages) { 375 if (usage.attributionTag != null && !subAttributionLabelsMap.containsKey( 376 usage.packageName)) { 377 subAttributionLabelsMap.put(usage.packageName, 378 getSubattributionLabelsForPackage(usage.packageName, usage.uid)); 379 } 380 } 381 } 382 383 /** 384 * Query attribution labels for a package 385 * 386 * @param packageName 387 * @param uid 388 * @return map of attribution tag -> attribution labels for a package 389 */ getSubattributionLabelsForPackage(String packageName, int uid)390 private ArrayMap<String, String> getSubattributionLabelsForPackage(String packageName, 391 int uid) { 392 ArrayMap<String, String> attributionLabelMap = new ArrayMap<>(); 393 UserHandle user = UserHandle.getUserHandleForUid(uid); 394 try { 395 if (!isSubattributionSupported(packageName, uid)) { 396 return attributionLabelMap; 397 } 398 Context userContext = getUserContext(user); 399 PackageInfo packageInfo = userContext.getPackageManager().getPackageInfo( 400 packageName, 401 PackageManager.PackageInfoFlags.of( 402 PackageManager.GET_PERMISSIONS | PackageManager.GET_ATTRIBUTIONS_LONG)); 403 Context pkgContext = userContext.createPackageContext(packageInfo.packageName, 0); 404 for (Attribution attribution : packageInfo.attributions) { 405 try { 406 String resourceForLabel = pkgContext.getString(attribution.getLabel()); 407 attributionLabelMap.put(attribution.getTag(), resourceForLabel); 408 } catch (Resources.NotFoundException e) { 409 // Shouldn't happen, do nothing 410 } 411 } 412 } catch (PackageManager.NameNotFoundException e) { 413 // Did not find the package, do nothing 414 } 415 return attributionLabelMap; 416 } 417 418 /** 419 * Returns true if the app satisfies subattribution policies and supports it 420 */ isSubattributionSupported(String packageName, int uid)421 private boolean isSubattributionSupported(String packageName, int uid) { 422 try { 423 if (!isLocationProvider(packageName)) { 424 return false; 425 } 426 PackageManager userPkgManager = 427 getUserContext(UserHandle.getUserHandleForUid(uid)).getPackageManager(); 428 ApplicationInfo appInfo = userPkgManager.getApplicationInfoAsUser(packageName, 429 PackageManager.ApplicationInfoFlags.of(0), 430 UserHandle.getUserId(uid)); 431 if (appInfo != null) { 432 return appInfo.areAttributionsUserVisible(); 433 } 434 return false; 435 } catch (PackageManager.NameNotFoundException e) { 436 return false; 437 } 438 } 439 440 /** 441 * @param packageName 442 * @return If the package is location provider 443 */ isLocationProvider(String packageName)444 private boolean isLocationProvider(String packageName) { 445 return Objects.requireNonNull( 446 mContext.getSystemService(LocationManager.class)).isProviderPackage(packageName); 447 } 448 449 /** 450 * Get the raw usages from the system, and then parse out the ones that are not recent enough, 451 * determine which permission group each belongs in, and removes duplicates (if the same app 452 * uses multiple permissions of the same group). Stores the package name, attribution tag, user, 453 * running/recent info, if the usage is a phone call, per permission group. 454 * 455 * @param opNames a list of op names to get usage for 456 * @return A map of permission group -> list of usages that are recent or running 457 */ getOpUsages(List<String> opNames)458 private Map<String, List<OpUsage>> getOpUsages(List<String> opNames) { 459 List<AppOpsManager.PackageOps> ops; 460 try { 461 ops = mAppOpsManager.getPackagesForOps(opNames.toArray(new String[opNames.size()])); 462 } catch (NullPointerException e) { 463 // older builds might not support all the app-ops requested 464 return Collections.emptyMap(); 465 } 466 467 long now = System.currentTimeMillis(); 468 long recentThreshold = getRecentThreshold(now); 469 long runningThreshold = getRunningThreshold(now); 470 int opFlags = OP_FLAGS_ALL_TRUSTED; 471 Map<String, Map<Integer, OpUsage>> usages = new ArrayMap<>(); 472 473 int numPkgOps = ops.size(); 474 for (int pkgOpNum = 0; pkgOpNum < numPkgOps; pkgOpNum++) { 475 AppOpsManager.PackageOps pkgOps = ops.get(pkgOpNum); 476 int uid = pkgOps.getUid(); 477 UserHandle user = UserHandle.getUserHandleForUid(uid); 478 String packageName = pkgOps.getPackageName(); 479 480 int numOpEntries = pkgOps.getOps().size(); 481 for (int opEntryNum = 0; opEntryNum < numOpEntries; opEntryNum++) { 482 AppOpsManager.OpEntry opEntry = pkgOps.getOps().get(opEntryNum); 483 String op = opEntry.getOpStr(); 484 List<String> attributionTags = 485 new ArrayList<>(opEntry.getAttributedOpEntries().keySet()); 486 487 488 int numAttrEntries = opEntry.getAttributedOpEntries().size(); 489 for (int attrOpEntryNum = 0; attrOpEntryNum < numAttrEntries; attrOpEntryNum++) { 490 String attributionTag = attributionTags.get(attrOpEntryNum); 491 AppOpsManager.AttributedOpEntry attrOpEntry = 492 opEntry.getAttributedOpEntries().get(attributionTag); 493 494 long lastAccessTime = attrOpEntry.getLastAccessTime(opFlags); 495 if (attrOpEntry.isRunning()) { 496 lastAccessTime = now; 497 } 498 499 if (lastAccessTime < recentThreshold && !attrOpEntry.isRunning()) { 500 continue; 501 } 502 503 boolean isRunning = attrOpEntry.isRunning() 504 || lastAccessTime >= runningThreshold; 505 506 OpUsage proxyUsage = null; 507 AppOpsManager.OpEventProxyInfo proxy = attrOpEntry.getLastProxyInfo(opFlags); 508 if (proxy != null && proxy.getPackageName() != null) { 509 proxyUsage = new OpUsage(proxy.getPackageName(), proxy.getAttributionTag(), 510 op, proxy.getUid(), lastAccessTime, isRunning, null); 511 } 512 513 String permGroupName = getGroupForOp(op); 514 OpUsage usage = new OpUsage(packageName, attributionTag, op, uid, 515 lastAccessTime, isRunning, proxyUsage); 516 517 Integer packageAttr = usage.getPackageIdHash(); 518 if (!usages.containsKey(permGroupName)) { 519 ArrayMap<Integer, OpUsage> map = new ArrayMap<>(); 520 map.put(packageAttr, usage); 521 usages.put(permGroupName, map); 522 } else { 523 Map<Integer, OpUsage> permGroupUsages = 524 usages.get(permGroupName); 525 if (!permGroupUsages.containsKey(packageAttr)) { 526 permGroupUsages.put(packageAttr, usage); 527 } else if (usage.lastAccessTime 528 > permGroupUsages.get(packageAttr).lastAccessTime) { 529 permGroupUsages.put(packageAttr, usage); 530 } 531 } 532 } 533 } 534 } 535 536 Map<String, List<OpUsage>> flattenedUsages = new ArrayMap<>(); 537 List<String> permGroups = new ArrayList<>(usages.keySet()); 538 for (int i = 0; i < permGroups.size(); i++) { 539 String permGroupName = permGroups.get(i); 540 flattenedUsages.put(permGroupName, new ArrayList<>(usages.get(permGroupName).values())); 541 } 542 return flattenedUsages; 543 } 544 formatLabelList(List<CharSequence> labels)545 private CharSequence formatLabelList(List<CharSequence> labels) { 546 return ListFormatter.getInstance().format(labels); 547 } 548 getUniqueUsagesWithLabels(String permGroup, List<OpUsage> usages)549 private ArrayMap<OpUsage, CharSequence> getUniqueUsagesWithLabels(String permGroup, 550 List<OpUsage> usages) { 551 ArrayMap<OpUsage, CharSequence> usagesAndLabels = new ArrayMap<>(); 552 553 if (usages == null || usages.isEmpty()) { 554 return usagesAndLabels; 555 } 556 557 ArrayMap<Integer, OpUsage> allUsages = new ArrayMap<>(); 558 // map of packageName and uid hash -> most recent non-proxy-related usage for that uid. 559 ArrayMap<Integer, OpUsage> mostRecentUsages = new ArrayMap<>(); 560 // set of all packages involved in a proxy usage 561 ArraySet<Integer> proxyPackages = new ArraySet<>(); 562 // map of usage -> list of proxy app labels 563 ArrayMap<OpUsage, ArrayList<CharSequence>> proxyLabels = new ArrayMap<>(); 564 // map of usage.proxy hash -> usage hash, telling us if a usage is a proxy 565 ArrayMap<Integer, OpUsage> proxies = new ArrayMap<>(); 566 567 for (int i = 0; i < usages.size(); i++) { 568 OpUsage usage = usages.get(i); 569 allUsages.put(usage.getPackageIdHash(), usage); 570 if (usage.proxy != null) { 571 proxies.put(usage.proxy.getPackageIdHash(), usage); 572 } 573 } 574 575 // find all possible end points for chains, and find the most recent of the rest of the uses 576 for (int usageNum = 0; usageNum < usages.size(); usageNum++) { 577 OpUsage usage = usages.get(usageNum); 578 if (usage == null) { 579 continue; 580 } 581 582 int usageAttr = usage.getPackageIdHash(); 583 // If this usage has a proxy, but is not a proxy, it is the end of a chain. 584 // TODO remove once camera converted 585 if (!proxies.containsKey(usageAttr) && usage.proxy != null 586 && !MICROPHONE.equals(permGroup)) { 587 proxyLabels.put(usage, new ArrayList<>()); 588 proxyPackages.add(usage.getPackageIdHash()); 589 } 590 // If this usage is not by the system, and is more recent than the next-most recent 591 // for it's uid and package name, save it. 592 int usageId = usage.getPackageIdHash(); 593 OpUsage lastMostRecent = mostRecentUsages.get(usageId); 594 if (shouldShowPackage(usage.packageName) && (lastMostRecent == null 595 || usage.lastAccessTime > lastMostRecent.lastAccessTime)) { 596 mostRecentUsages.put(usageId, usage); 597 } 598 } 599 600 // get all the proxy labels 601 for (int numStart = 0; numStart < proxyLabels.size(); numStart++) { 602 OpUsage start = proxyLabels.keyAt(numStart); 603 // Remove any non-proxy usage for the starting package 604 mostRecentUsages.remove(start.getPackageIdHash()); 605 OpUsage currentUsage = proxyLabels.keyAt(numStart); 606 ArrayList<CharSequence> proxyLabelList = proxyLabels.get(currentUsage); 607 if (currentUsage == null || proxyLabelList == null) { 608 continue; 609 } 610 int iterNum = 0; 611 int maxUsages = allUsages.size(); 612 while (currentUsage.proxy != null) { 613 614 if (allUsages.containsKey(currentUsage.proxy.getPackageIdHash())) { 615 currentUsage = allUsages.get(currentUsage.proxy.getPackageIdHash()); 616 } else { 617 // We are missing the proxy usage. This may be because it's a one-step trusted 618 // proxy. Check if we should show the proxy label, and show it, if so. 619 OpUsage proxy = currentUsage.proxy; 620 if (shouldShowPackage(proxy.packageName)) { 621 currentUsage = proxy; 622 // We've effectively added one usage, so increment the max number of usages 623 maxUsages++; 624 } else { 625 break; 626 } 627 } 628 629 if (currentUsage == null || iterNum == maxUsages 630 || currentUsage.getPackageIdHash() == start.getPackageIdHash()) { 631 // We have an invalid state, or a cycle, so break 632 break; 633 } 634 635 proxyPackages.add(currentUsage.getPackageIdHash()); 636 // Don't add an app label for the main app, or the system app 637 if (!currentUsage.packageName.equals(start.packageName) 638 && shouldShowPackage(currentUsage.packageName)) { 639 try { 640 PackageManager userPkgManager = 641 getUserContext(currentUsage.getUser()).getPackageManager(); 642 ApplicationInfo appInfo = userPkgManager.getApplicationInfo( 643 currentUsage.packageName, 0); 644 CharSequence appLabel = appInfo.loadLabel(userPkgManager); 645 // If we don't already have the app label add it 646 if (!proxyLabelList.contains(appLabel)) { 647 proxyLabelList.add(appLabel); 648 } 649 } catch (PackageManager.NameNotFoundException e) { 650 // Ignore 651 } 652 } 653 iterNum++; 654 } 655 656 // TODO ntmyren: remove this proxy logic once camera is converted to AttributionSource 657 // For now: don't add mic proxy usages 658 if (!MICROPHONE.equals(permGroup)) { 659 usagesAndLabels.put(start, 660 proxyLabelList.isEmpty() ? null : formatLabelList(proxyLabelList)); 661 } 662 } 663 664 synchronized (mAttributionChains) { 665 for (int i = 0; i < mAttributionChains.size(); i++) { 666 List<AccessChainLink> usageList = mAttributionChains.valueAt(i); 667 int lastVisible = usageList.size() - 1; 668 // TODO ntmyren: remove this mic code once camera is converted to AttributionSource 669 // if the list is empty or incomplete, do not show it. 670 if (usageList.isEmpty() || !usageList.get(lastVisible).isEnd() 671 || !usageList.get(0).isStart() 672 || !permGroup.equals(getGroupForOp(usageList.get(0).usage.op)) 673 || !MICROPHONE.equals(permGroup)) { 674 continue; 675 } 676 677 //TODO ntmyren: remove once camera etc. etc. 678 for (AccessChainLink link : usageList) { 679 proxyPackages.add(link.usage.getPackageIdHash()); 680 } 681 682 AccessChainLink start = usageList.get(0); 683 AccessChainLink lastVisibleLink = usageList.get(lastVisible); 684 while (lastVisible > 0 && !shouldShowPackage(lastVisibleLink.usage.packageName)) { 685 lastVisible--; 686 lastVisibleLink = usageList.get(lastVisible); 687 } 688 String proxyLabel = null; 689 if (!lastVisibleLink.usage.packageName.equals(start.usage.packageName)) { 690 try { 691 PackageManager userPkgManager = 692 getUserContext(lastVisibleLink.usage.getUser()).getPackageManager(); 693 ApplicationInfo appInfo = userPkgManager.getApplicationInfo( 694 lastVisibleLink.usage.packageName, 0); 695 proxyLabel = appInfo.loadLabel(userPkgManager).toString(); 696 } catch (PackageManager.NameNotFoundException e) { 697 // do nothing 698 } 699 } 700 usagesAndLabels.put(start.usage, proxyLabel); 701 } 702 } 703 704 for (int packageHash : mostRecentUsages.keySet()) { 705 if (!proxyPackages.contains(packageHash)) { 706 usagesAndLabels.put(mostRecentUsages.get(packageHash), null); 707 } 708 } 709 710 return usagesAndLabels; 711 } 712 shouldShowPackage(String packageName)713 private boolean shouldShowPackage(String packageName) { 714 return PermissionManager.shouldShowPackageForIndicatorCached(mContext, packageName); 715 } 716 717 /** 718 * Represents the usage of an App op by a particular package and attribution 719 */ 720 private static class OpUsage { 721 722 public final String packageName; 723 public final String attributionTag; 724 public final String op; 725 public final int uid; 726 public final long lastAccessTime; 727 public final OpUsage proxy; 728 public final boolean isRunning; 729 OpUsage(String packageName, String attributionTag, String op, int uid, long lastAccessTime, boolean isRunning, OpUsage proxy)730 OpUsage(String packageName, String attributionTag, String op, int uid, long lastAccessTime, 731 boolean isRunning, OpUsage proxy) { 732 this.packageName = packageName; 733 this.attributionTag = attributionTag; 734 this.op = op; 735 this.uid = uid; 736 this.lastAccessTime = lastAccessTime; 737 this.isRunning = isRunning; 738 this.proxy = proxy; 739 } 740 getUser()741 public UserHandle getUser() { 742 return UserHandle.getUserHandleForUid(uid); 743 } 744 getPackageIdHash()745 public int getPackageIdHash() { 746 return Objects.hash(packageName, uid); 747 } 748 749 @Override hashCode()750 public int hashCode() { 751 return Objects.hash(packageName, attributionTag, op, uid, lastAccessTime, isRunning); 752 } 753 754 @Override equals(Object obj)755 public boolean equals(Object obj) { 756 if (!(obj instanceof OpUsage)) { 757 return false; 758 } 759 OpUsage other = (OpUsage) obj; 760 return Objects.equals(packageName, other.packageName) && Objects.equals(attributionTag, 761 other.attributionTag) && Objects.equals(op, other.op) && uid == other.uid 762 && lastAccessTime == other.lastAccessTime && isRunning == other.isRunning; 763 } 764 } 765 766 private static class AccessChainLink { 767 public final OpUsage usage; 768 public final @AttributionFlags int flags; 769 AccessChainLink(String op, String packageName, String attributionTag, int uid, int flags)770 AccessChainLink(String op, String packageName, String attributionTag, int uid, 771 int flags) { 772 this.usage = new OpUsage(packageName, attributionTag, op, uid, 773 System.currentTimeMillis(), true, null); 774 this.flags = flags; 775 } 776 isEnd()777 public boolean isEnd() { 778 return (flags & ATTRIBUTION_FLAG_ACCESSOR) != 0; 779 } 780 isStart()781 public boolean isStart() { 782 return (flags & ATTRIBUTION_FLAG_RECEIVER) != 0; 783 } 784 785 @Override equals(Object obj)786 public boolean equals(Object obj) { 787 if (!(obj instanceof AccessChainLink)) { 788 return false; 789 } 790 AccessChainLink other = (AccessChainLink) obj; 791 return other.flags == flags && packageAndOpEquals(other.usage.op, 792 other.usage.packageName, other.usage.attributionTag, other.usage.uid); 793 } 794 packageAndOpEquals(String op, String packageName, String attributionTag, int uid)795 public boolean packageAndOpEquals(String op, String packageName, String attributionTag, 796 int uid) { 797 return Objects.equals(op, usage.op) && Objects.equals(packageName, usage.packageName) 798 && Objects.equals(attributionTag, usage.attributionTag) && uid == usage.uid; 799 } 800 } 801 } 802