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