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