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