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