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.GET_PERMISSIONS | PackageManager.GET_ATTRIBUTIONS); 402 Context pkgContext = userContext.createPackageContext(packageInfo.packageName, 0); 403 for (Attribution attribution : packageInfo.attributions) { 404 try { 405 String resourceForLabel = pkgContext.getString(attribution.getLabel()); 406 attributionLabelMap.put(attribution.getTag(), resourceForLabel); 407 } catch (Resources.NotFoundException e) { 408 // Shouldn't happen, do nothing 409 } 410 } 411 } catch (PackageManager.NameNotFoundException e) { 412 // Did not find the package, do nothing 413 } 414 return attributionLabelMap; 415 } 416 417 /** 418 * Returns true if the app satisfies subattribution policies and supports it 419 */ isSubattributionSupported(String packageName, int uid)420 private boolean isSubattributionSupported(String packageName, int uid) { 421 try { 422 if (!isLocationProvider(packageName)) { 423 return false; 424 } 425 PackageManager userPkgManager = 426 getUserContext(UserHandle.getUserHandleForUid(uid)).getPackageManager(); 427 ApplicationInfo appInfo = userPkgManager.getApplicationInfoAsUser(packageName, 428 PackageManager.ApplicationInfoFlags.of(0), 429 UserHandle.getUserId(uid)); 430 if (appInfo != null) { 431 return appInfo.areAttributionsUserVisible(); 432 } 433 return false; 434 } catch (PackageManager.NameNotFoundException e) { 435 return false; 436 } 437 } 438 439 /** 440 * @param packageName 441 * @return If the package is location provider 442 */ isLocationProvider(String packageName)443 private boolean isLocationProvider(String packageName) { 444 return Objects.requireNonNull( 445 mContext.getSystemService(LocationManager.class)).isProviderPackage(packageName); 446 } 447 448 /** 449 * Get the raw usages from the system, and then parse out the ones that are not recent enough, 450 * determine which permission group each belongs in, and removes duplicates (if the same app 451 * uses multiple permissions of the same group). Stores the package name, attribution tag, user, 452 * running/recent info, if the usage is a phone call, per permission group. 453 * 454 * @param opNames a list of op names to get usage for 455 * @return A map of permission group -> list of usages that are recent or running 456 */ getOpUsages(List<String> opNames)457 private Map<String, List<OpUsage>> getOpUsages(List<String> opNames) { 458 List<AppOpsManager.PackageOps> ops; 459 try { 460 ops = mAppOpsManager.getPackagesForOps(opNames.toArray(new String[opNames.size()])); 461 } catch (NullPointerException e) { 462 // older builds might not support all the app-ops requested 463 return Collections.emptyMap(); 464 } 465 466 long now = System.currentTimeMillis(); 467 long recentThreshold = getRecentThreshold(now); 468 long runningThreshold = getRunningThreshold(now); 469 int opFlags = OP_FLAGS_ALL_TRUSTED; 470 Map<String, Map<Integer, OpUsage>> usages = new ArrayMap<>(); 471 472 int numPkgOps = ops.size(); 473 for (int pkgOpNum = 0; pkgOpNum < numPkgOps; pkgOpNum++) { 474 AppOpsManager.PackageOps pkgOps = ops.get(pkgOpNum); 475 int uid = pkgOps.getUid(); 476 UserHandle user = UserHandle.getUserHandleForUid(uid); 477 String packageName = pkgOps.getPackageName(); 478 479 int numOpEntries = pkgOps.getOps().size(); 480 for (int opEntryNum = 0; opEntryNum < numOpEntries; opEntryNum++) { 481 AppOpsManager.OpEntry opEntry = pkgOps.getOps().get(opEntryNum); 482 String op = opEntry.getOpStr(); 483 List<String> attributionTags = 484 new ArrayList<>(opEntry.getAttributedOpEntries().keySet()); 485 486 487 int numAttrEntries = opEntry.getAttributedOpEntries().size(); 488 for (int attrOpEntryNum = 0; attrOpEntryNum < numAttrEntries; attrOpEntryNum++) { 489 String attributionTag = attributionTags.get(attrOpEntryNum); 490 AppOpsManager.AttributedOpEntry attrOpEntry = 491 opEntry.getAttributedOpEntries().get(attributionTag); 492 493 long lastAccessTime = attrOpEntry.getLastAccessTime(opFlags); 494 if (attrOpEntry.isRunning()) { 495 lastAccessTime = now; 496 } 497 498 if (lastAccessTime < recentThreshold && !attrOpEntry.isRunning()) { 499 continue; 500 } 501 502 boolean isRunning = attrOpEntry.isRunning() 503 || lastAccessTime >= runningThreshold; 504 505 OpUsage proxyUsage = null; 506 AppOpsManager.OpEventProxyInfo proxy = attrOpEntry.getLastProxyInfo(opFlags); 507 if (proxy != null && proxy.getPackageName() != null) { 508 proxyUsage = new OpUsage(proxy.getPackageName(), proxy.getAttributionTag(), 509 op, proxy.getUid(), lastAccessTime, isRunning, null); 510 } 511 512 String permGroupName = getGroupForOp(op); 513 OpUsage usage = new OpUsage(packageName, attributionTag, op, uid, 514 lastAccessTime, isRunning, proxyUsage); 515 516 Integer packageAttr = usage.getPackageIdHash(); 517 if (!usages.containsKey(permGroupName)) { 518 ArrayMap<Integer, OpUsage> map = new ArrayMap<>(); 519 map.put(packageAttr, usage); 520 usages.put(permGroupName, map); 521 } else { 522 Map<Integer, OpUsage> permGroupUsages = 523 usages.get(permGroupName); 524 if (!permGroupUsages.containsKey(packageAttr)) { 525 permGroupUsages.put(packageAttr, usage); 526 } else if (usage.lastAccessTime 527 > permGroupUsages.get(packageAttr).lastAccessTime) { 528 permGroupUsages.put(packageAttr, usage); 529 } 530 } 531 } 532 } 533 } 534 535 Map<String, List<OpUsage>> flattenedUsages = new ArrayMap<>(); 536 List<String> permGroups = new ArrayList<>(usages.keySet()); 537 for (int i = 0; i < permGroups.size(); i++) { 538 String permGroupName = permGroups.get(i); 539 flattenedUsages.put(permGroupName, new ArrayList<>(usages.get(permGroupName).values())); 540 } 541 return flattenedUsages; 542 } 543 formatLabelList(List<CharSequence> labels)544 private CharSequence formatLabelList(List<CharSequence> labels) { 545 return ListFormatter.getInstance().format(labels); 546 } 547 getUniqueUsagesWithLabels(String permGroup, List<OpUsage> usages)548 private ArrayMap<OpUsage, CharSequence> getUniqueUsagesWithLabels(String permGroup, 549 List<OpUsage> usages) { 550 ArrayMap<OpUsage, CharSequence> usagesAndLabels = new ArrayMap<>(); 551 552 if (usages == null || usages.isEmpty()) { 553 return usagesAndLabels; 554 } 555 556 ArrayMap<Integer, OpUsage> allUsages = new ArrayMap<>(); 557 // map of packageName and uid hash -> most recent non-proxy-related usage for that uid. 558 ArrayMap<Integer, OpUsage> mostRecentUsages = new ArrayMap<>(); 559 // set of all packages involved in a proxy usage 560 ArraySet<Integer> proxyPackages = new ArraySet<>(); 561 // map of usage -> list of proxy app labels 562 ArrayMap<OpUsage, ArrayList<CharSequence>> proxyLabels = new ArrayMap<>(); 563 // map of usage.proxy hash -> usage hash, telling us if a usage is a proxy 564 ArrayMap<Integer, OpUsage> proxies = new ArrayMap<>(); 565 566 for (int i = 0; i < usages.size(); i++) { 567 OpUsage usage = usages.get(i); 568 allUsages.put(usage.getPackageIdHash(), usage); 569 if (usage.proxy != null) { 570 proxies.put(usage.proxy.getPackageIdHash(), usage); 571 } 572 } 573 574 // find all possible end points for chains, and find the most recent of the rest of the uses 575 for (int usageNum = 0; usageNum < usages.size(); usageNum++) { 576 OpUsage usage = usages.get(usageNum); 577 if (usage == null) { 578 continue; 579 } 580 581 int usageAttr = usage.getPackageIdHash(); 582 // If this usage has a proxy, but is not a proxy, it is the end of a chain. 583 // TODO remove once camera converted 584 if (!proxies.containsKey(usageAttr) && usage.proxy != null 585 && !MICROPHONE.equals(permGroup)) { 586 proxyLabels.put(usage, new ArrayList<>()); 587 proxyPackages.add(usage.getPackageIdHash()); 588 } 589 // If this usage is not by the system, and is more recent than the next-most recent 590 // for it's uid and package name, save it. 591 int usageId = usage.getPackageIdHash(); 592 OpUsage lastMostRecent = mostRecentUsages.get(usageId); 593 if (shouldShowPackage(usage.packageName) && (lastMostRecent == null 594 || usage.lastAccessTime > lastMostRecent.lastAccessTime)) { 595 mostRecentUsages.put(usageId, usage); 596 } 597 } 598 599 // get all the proxy labels 600 for (int numStart = 0; numStart < proxyLabels.size(); numStart++) { 601 OpUsage start = proxyLabels.keyAt(numStart); 602 // Remove any non-proxy usage for the starting package 603 mostRecentUsages.remove(start.getPackageIdHash()); 604 OpUsage currentUsage = proxyLabels.keyAt(numStart); 605 ArrayList<CharSequence> proxyLabelList = proxyLabels.get(currentUsage); 606 if (currentUsage == null || proxyLabelList == null) { 607 continue; 608 } 609 int iterNum = 0; 610 int maxUsages = allUsages.size(); 611 while (currentUsage.proxy != null) { 612 613 if (allUsages.containsKey(currentUsage.proxy.getPackageIdHash())) { 614 currentUsage = allUsages.get(currentUsage.proxy.getPackageIdHash()); 615 } else { 616 // We are missing the proxy usage. This may be because it's a one-step trusted 617 // proxy. Check if we should show the proxy label, and show it, if so. 618 OpUsage proxy = currentUsage.proxy; 619 if (shouldShowPackage(proxy.packageName)) { 620 currentUsage = proxy; 621 // We've effectively added one usage, so increment the max number of usages 622 maxUsages++; 623 } else { 624 break; 625 } 626 } 627 628 if (currentUsage == null || iterNum == maxUsages 629 || currentUsage.getPackageIdHash() == start.getPackageIdHash()) { 630 // We have an invalid state, or a cycle, so break 631 break; 632 } 633 634 proxyPackages.add(currentUsage.getPackageIdHash()); 635 // Don't add an app label for the main app, or the system app 636 if (!currentUsage.packageName.equals(start.packageName) 637 && shouldShowPackage(currentUsage.packageName)) { 638 try { 639 PackageManager userPkgManager = 640 getUserContext(currentUsage.getUser()).getPackageManager(); 641 ApplicationInfo appInfo = userPkgManager.getApplicationInfo( 642 currentUsage.packageName, 0); 643 CharSequence appLabel = appInfo.loadLabel(userPkgManager); 644 // If we don't already have the app label add it 645 if (!proxyLabelList.contains(appLabel)) { 646 proxyLabelList.add(appLabel); 647 } 648 } catch (PackageManager.NameNotFoundException e) { 649 // Ignore 650 } 651 } 652 iterNum++; 653 } 654 655 // TODO ntmyren: remove this proxy logic once camera is converted to AttributionSource 656 // For now: don't add mic proxy usages 657 if (!MICROPHONE.equals(permGroup)) { 658 usagesAndLabels.put(start, 659 proxyLabelList.isEmpty() ? null : formatLabelList(proxyLabelList)); 660 } 661 } 662 663 synchronized (mAttributionChains) { 664 for (int i = 0; i < mAttributionChains.size(); i++) { 665 List<AccessChainLink> usageList = mAttributionChains.valueAt(i); 666 int lastVisible = usageList.size() - 1; 667 // TODO ntmyren: remove this mic code once camera is converted to AttributionSource 668 // if the list is empty or incomplete, do not show it. 669 if (usageList.isEmpty() || !usageList.get(lastVisible).isEnd() 670 || !usageList.get(0).isStart() 671 || !permGroup.equals(getGroupForOp(usageList.get(0).usage.op)) 672 || !MICROPHONE.equals(permGroup)) { 673 continue; 674 } 675 676 //TODO ntmyren: remove once camera etc. etc. 677 for (AccessChainLink link : usageList) { 678 proxyPackages.add(link.usage.getPackageIdHash()); 679 } 680 681 AccessChainLink start = usageList.get(0); 682 AccessChainLink lastVisibleLink = usageList.get(lastVisible); 683 while (lastVisible > 0 && !shouldShowPackage(lastVisibleLink.usage.packageName)) { 684 lastVisible--; 685 lastVisibleLink = usageList.get(lastVisible); 686 } 687 String proxyLabel = null; 688 if (!lastVisibleLink.usage.packageName.equals(start.usage.packageName)) { 689 try { 690 PackageManager userPkgManager = 691 getUserContext(lastVisibleLink.usage.getUser()).getPackageManager(); 692 ApplicationInfo appInfo = userPkgManager.getApplicationInfo( 693 lastVisibleLink.usage.packageName, 0); 694 proxyLabel = appInfo.loadLabel(userPkgManager).toString(); 695 } catch (PackageManager.NameNotFoundException e) { 696 // do nothing 697 } 698 } 699 usagesAndLabels.put(start.usage, proxyLabel); 700 } 701 } 702 703 for (int packageHash : mostRecentUsages.keySet()) { 704 if (!proxyPackages.contains(packageHash)) { 705 usagesAndLabels.put(mostRecentUsages.get(packageHash), null); 706 } 707 } 708 709 return usagesAndLabels; 710 } 711 shouldShowPackage(String packageName)712 private boolean shouldShowPackage(String packageName) { 713 return PermissionManager.shouldShowPackageForIndicatorCached(mContext, packageName); 714 } 715 716 /** 717 * Represents the usage of an App op by a particular package and attribution 718 */ 719 private static class OpUsage { 720 721 public final String packageName; 722 public final String attributionTag; 723 public final String op; 724 public final int uid; 725 public final long lastAccessTime; 726 public final OpUsage proxy; 727 public final boolean isRunning; 728 OpUsage(String packageName, String attributionTag, String op, int uid, long lastAccessTime, boolean isRunning, OpUsage proxy)729 OpUsage(String packageName, String attributionTag, String op, int uid, long lastAccessTime, 730 boolean isRunning, OpUsage proxy) { 731 this.packageName = packageName; 732 this.attributionTag = attributionTag; 733 this.op = op; 734 this.uid = uid; 735 this.lastAccessTime = lastAccessTime; 736 this.isRunning = isRunning; 737 this.proxy = proxy; 738 } 739 getUser()740 public UserHandle getUser() { 741 return UserHandle.getUserHandleForUid(uid); 742 } 743 getPackageIdHash()744 public int getPackageIdHash() { 745 return Objects.hash(packageName, uid); 746 } 747 748 @Override hashCode()749 public int hashCode() { 750 return Objects.hash(packageName, attributionTag, op, uid, lastAccessTime, isRunning); 751 } 752 753 @Override equals(Object obj)754 public boolean equals(Object obj) { 755 if (!(obj instanceof OpUsage)) { 756 return false; 757 } 758 OpUsage other = (OpUsage) obj; 759 return Objects.equals(packageName, other.packageName) && Objects.equals(attributionTag, 760 other.attributionTag) && Objects.equals(op, other.op) && uid == other.uid 761 && lastAccessTime == other.lastAccessTime && isRunning == other.isRunning; 762 } 763 } 764 765 private static class AccessChainLink { 766 public final OpUsage usage; 767 public final @AttributionFlags int flags; 768 AccessChainLink(String op, String packageName, String attributionTag, int uid, int flags)769 AccessChainLink(String op, String packageName, String attributionTag, int uid, 770 int flags) { 771 this.usage = new OpUsage(packageName, attributionTag, op, uid, 772 System.currentTimeMillis(), true, null); 773 this.flags = flags; 774 } 775 isEnd()776 public boolean isEnd() { 777 return (flags & ATTRIBUTION_FLAG_ACCESSOR) != 0; 778 } 779 isStart()780 public boolean isStart() { 781 return (flags & ATTRIBUTION_FLAG_RECEIVER) != 0; 782 } 783 784 @Override equals(Object obj)785 public boolean equals(Object obj) { 786 if (!(obj instanceof AccessChainLink)) { 787 return false; 788 } 789 AccessChainLink other = (AccessChainLink) obj; 790 return other.flags == flags && packageAndOpEquals(other.usage.op, 791 other.usage.packageName, other.usage.attributionTag, other.usage.uid); 792 } 793 packageAndOpEquals(String op, String packageName, String attributionTag, int uid)794 public boolean packageAndOpEquals(String op, String packageName, String attributionTag, 795 int uid) { 796 return Objects.equals(op, usage.op) && Objects.equals(packageName, usage.packageName) 797 && Objects.equals(attributionTag, usage.attributionTag) && uid == usage.uid; 798 } 799 } 800 } 801