1 /*
2  * Copyright (C) 2021 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 com.android.settingslib.applications;
18 
19 
20 import android.app.AppOpsManager;
21 import android.content.Context;
22 import android.content.PermissionChecker;
23 import android.content.pm.ApplicationInfo;
24 import android.content.pm.PackageManager;
25 import android.content.pm.PackageManager.NameNotFoundException;
26 import android.content.pm.UserProperties;
27 import android.graphics.drawable.Drawable;
28 import android.os.UserHandle;
29 import android.os.UserManager;
30 import android.permission.PermissionManager;
31 import android.text.format.DateUtils;
32 import android.util.ArrayMap;
33 import android.util.IconDrawableFactory;
34 import android.util.Log;
35 
36 import androidx.annotation.VisibleForTesting;
37 
38 import java.time.Clock;
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.Comparator;
42 import java.util.List;
43 
44 /**
45  * Retrieval of app ops information for the specified ops.
46  */
47 public class RecentAppOpsAccess {
48     @VisibleForTesting
49     static final int[] LOCATION_OPS = new int[]{
50             AppOpsManager.OP_FINE_LOCATION,
51             AppOpsManager.OP_COARSE_LOCATION,
52     };
53     private static final int[] MICROPHONE_OPS = new int[]{
54             AppOpsManager.OP_RECORD_AUDIO,
55             AppOpsManager.OP_PHONE_CALL_MICROPHONE,
56     };
57     private static final int[] CAMERA_OPS = new int[]{
58             AppOpsManager.OP_CAMERA,
59     };
60 
61 
62     private static final String TAG = RecentAppOpsAccess.class.getSimpleName();
63     @VisibleForTesting
64     public static final String ANDROID_SYSTEM_PACKAGE_NAME = "android";
65 
66     // Keep last 24 hours of access app information.
67     private static final long RECENT_TIME_INTERVAL_MILLIS = DateUtils.DAY_IN_MILLIS;
68 
69     /** The flags for querying ops that are trusted for showing in the UI. */
70     public static final int TRUSTED_STATE_FLAGS = AppOpsManager.OP_FLAG_SELF
71             | AppOpsManager.OP_FLAG_UNTRUSTED_PROXY
72             | AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
73 
74     private final PackageManager mPackageManager;
75     private final Context mContext;
76     private final int[] mOps;
77     private final IconDrawableFactory mDrawableFactory;
78     private final Clock mClock;
79 
RecentAppOpsAccess(Context context, int[] ops)80     public RecentAppOpsAccess(Context context, int[] ops) {
81         this(context, Clock.systemDefaultZone(), ops);
82     }
83 
84     @VisibleForTesting
RecentAppOpsAccess(Context context, Clock clock, int[] ops)85     RecentAppOpsAccess(Context context, Clock clock, int[] ops) {
86         mContext = context;
87         mPackageManager = context.getPackageManager();
88         mOps = ops;
89         mDrawableFactory = IconDrawableFactory.newInstance(context);
90         mClock = clock;
91     }
92 
93     /**
94      * Creates an instance of {@link RecentAppOpsAccess} for location (coarse and fine) access.
95      */
createForLocation(Context context)96     public static RecentAppOpsAccess createForLocation(Context context) {
97         return new RecentAppOpsAccess(context, LOCATION_OPS);
98     }
99 
100     /**
101      * Creates an instance of {@link RecentAppOpsAccess} for microphone access.
102      */
createForMicrophone(Context context)103     public static RecentAppOpsAccess createForMicrophone(Context context) {
104         return new RecentAppOpsAccess(context, MICROPHONE_OPS);
105     }
106 
107     /**
108      * Creates an instance of {@link RecentAppOpsAccess} for camera access.
109      */
createForCamera(Context context)110     public static RecentAppOpsAccess createForCamera(Context context) {
111         return new RecentAppOpsAccess(context, CAMERA_OPS);
112     }
113 
114     /**
115      * Fills a list of applications which queried for access recently within specified time.
116      * Apps are sorted by recency. Apps with more recent accesses are in the front.
117      */
118     @VisibleForTesting
getAppList(boolean showSystemApps)119     public List<Access> getAppList(boolean showSystemApps) {
120         // Retrieve a access usage list from AppOps
121         AppOpsManager aoManager = mContext.getSystemService(AppOpsManager.class);
122         List<AppOpsManager.PackageOps> appOps = aoManager.getPackagesForOps(mOps);
123 
124         final int appOpsCount = appOps != null ? appOps.size() : 0;
125 
126         // Process the AppOps list and generate a preference list.
127         ArrayList<Access> accesses = new ArrayList<>(appOpsCount);
128         final long now = mClock.millis();
129         final UserManager um = mContext.getSystemService(UserManager.class);
130         final List<UserHandle> profiles = um.getUserProfiles();
131         ArrayMap<UserHandle, Boolean> shouldHideAppsByUsers = new ArrayMap<>();
132 
133         for (int i = 0; i < appOpsCount; ++i) {
134             AppOpsManager.PackageOps ops = appOps.get(i);
135             String packageName = ops.getPackageName();
136             int uid = ops.getUid();
137             UserHandle user = UserHandle.getUserHandleForUid(uid);
138 
139             if (!shouldHideAppsByUsers.containsKey(user)) {
140                 shouldHideAppsByUsers.put(user, shouldHideUser(um, user));
141             }
142 
143             // Don't show apps belonging to background users except for profiles that shouldn't
144             // be shown in quiet mode.
145             if (!profiles.contains(user) || shouldHideAppsByUsers.get(user)) {
146                 continue;
147             }
148 
149             // Don't show apps that do not have user sensitive location permissions
150             boolean showApp = true;
151             if (!showSystemApps) {
152                 for (int op : mOps) {
153                     final String permission = AppOpsManager.opToPermission(op);
154                     if (permission == null) {
155                         // Some ops like OP_PHONE_CALL_MICROPHONE don't have corresponding
156                         // permissions. No need to check in this case.
157                         continue;
158                     }
159                     final int permissionFlags = mPackageManager.getPermissionFlags(permission,
160                             packageName,
161                             user);
162                     if (PermissionChecker.checkPermissionForPreflight(mContext, permission,
163                             PermissionChecker.PID_UNKNOWN, uid, packageName)
164                             == PermissionChecker.PERMISSION_GRANTED) {
165                         if ((permissionFlags
166                                 & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED)
167                                 == 0) {
168                             showApp = false;
169                             break;
170                         }
171                     } else {
172                         if ((permissionFlags
173                                 & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED) == 0) {
174                             showApp = false;
175                             break;
176                         }
177                     }
178                 }
179             }
180             if (showApp && PermissionManager.shouldShowPackageForIndicatorCached(mContext,
181                     packageName)) {
182                 Access access = getAccessFromOps(now, ops);
183                 if (access != null) {
184                     accesses.add(access);
185                 }
186             }
187         }
188         return accesses;
189     }
190 
191     /**
192      * Gets a list of apps that accessed the app op recently, sorting by recency.
193      *
194      * @param showSystemApps whether includes system apps in the list.
195      * @return the list of apps that recently accessed the app op.
196      */
getAppListSorted(boolean showSystemApps)197     public List<Access> getAppListSorted(boolean showSystemApps) {
198         List<Access> accesses = getAppList(showSystemApps);
199         // Sort the list of Access by recency. Most recent accesses first.
200         Collections.sort(accesses, Collections.reverseOrder(new Comparator<Access>() {
201             @Override
202             public int compare(Access access1, Access access2) {
203                 return Long.compare(access1.accessFinishTime, access2.accessFinishTime);
204             }
205         }));
206         return accesses;
207     }
208 
shouldHideUser(UserManager userManager, UserHandle userHandle)209     private boolean shouldHideUser(UserManager userManager, UserHandle userHandle) {
210         if (android.multiuser.Flags.enablePrivateSpaceFeatures()
211                 && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) {
212             return userManager.isQuietModeEnabled(userHandle)
213                     && userManager.getUserProperties(userHandle).getShowInQuietMode()
214                             == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN;
215         }
216         return false;
217     }
218 
219     /**
220      * Creates a Access entry for the given PackageOps.
221      *
222      * This method examines the time interval of the PackageOps first. If the PackageOps is older
223      * than the designated interval, this method ignores the PackageOps object and returns null.
224      * When the PackageOps is fresh enough, this method returns a Access object for the package
225      */
getAccessFromOps(long now, AppOpsManager.PackageOps ops)226     private Access getAccessFromOps(long now,
227             AppOpsManager.PackageOps ops) {
228         String packageName = ops.getPackageName();
229         List<AppOpsManager.OpEntry> entries = ops.getOps();
230         long accessFinishTime = 0L;
231         // Earliest time for a access to end and still be shown in list.
232         long recentAccessCutoffTime = now - RECENT_TIME_INTERVAL_MILLIS;
233         // Compute the most recent access time from all op entries.
234         for (AppOpsManager.OpEntry entry : entries) {
235             long lastAccessTime = entry.getLastAccessTime(TRUSTED_STATE_FLAGS);
236             if (lastAccessTime > accessFinishTime) {
237                 accessFinishTime = lastAccessTime;
238             }
239         }
240         // Bail out if the entry is out of date.
241         if (accessFinishTime < recentAccessCutoffTime) {
242             return null;
243         }
244 
245         // The package is fresh enough, continue.
246         int uid = ops.getUid();
247         int userId = UserHandle.getUserId(uid);
248 
249         Access access = null;
250         try {
251             ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser(
252                     packageName, PackageManager.GET_META_DATA, userId);
253             if (appInfo == null) {
254                 Log.w(TAG, "Null application info retrieved for package " + packageName
255                         + ", userId " + userId);
256                 return null;
257             }
258 
259             final UserHandle userHandle = new UserHandle(userId);
260             Drawable icon = mDrawableFactory.getBadgedIcon(appInfo, userId);
261             CharSequence appLabel = mPackageManager.getApplicationLabel(appInfo);
262             CharSequence badgedAppLabel = mPackageManager.getUserBadgedLabel(appLabel, userHandle);
263             if (appLabel.toString().contentEquals(badgedAppLabel)) {
264                 // If badged label is not different from original then no need for it as
265                 // a separate content description.
266                 badgedAppLabel = null;
267             }
268             access = new Access(packageName, userHandle, icon, appLabel, badgedAppLabel,
269                     accessFinishTime);
270         } catch (NameNotFoundException e) {
271             Log.w(TAG, "package name not found for " + packageName + ", userId " + userId);
272         }
273         return access;
274     }
275 
276     /**
277      * Information about when an app last accessed a particular app op.
278      */
279     public static class Access {
280         public final String packageName;
281         public final UserHandle userHandle;
282         public final Drawable icon;
283         public final CharSequence label;
284         public final CharSequence contentDescription;
285         public final long accessFinishTime;
286 
Access(String packageName, UserHandle userHandle, Drawable icon, CharSequence label, CharSequence contentDescription, long accessFinishTime)287         public Access(String packageName, UserHandle userHandle, Drawable icon,
288                 CharSequence label, CharSequence contentDescription,
289                 long accessFinishTime) {
290             this.packageName = packageName;
291             this.userHandle = userHandle;
292             this.icon = icon;
293             this.label = label;
294             this.contentDescription = contentDescription;
295             this.accessFinishTime = accessFinishTime;
296         }
297     }
298 }
299