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