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