1 /* 2 * Copyright (C) 2015 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.location; 18 19 import android.app.AppOpsManager; 20 import android.content.Context; 21 import android.content.PermissionChecker; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageManager; 24 import android.content.pm.PackageManager.NameNotFoundException; 25 import android.graphics.drawable.Drawable; 26 import android.os.UserHandle; 27 import android.os.UserManager; 28 import android.text.format.DateUtils; 29 import android.util.IconDrawableFactory; 30 import android.util.Log; 31 32 import androidx.annotation.VisibleForTesting; 33 34 import java.util.ArrayList; 35 import java.util.Collections; 36 import java.util.Comparator; 37 import java.util.List; 38 39 /** 40 * Retrieves the information of applications which accessed location recently. 41 */ 42 public class RecentLocationApps { 43 private static final String TAG = RecentLocationApps.class.getSimpleName(); 44 @VisibleForTesting 45 static final String ANDROID_SYSTEM_PACKAGE_NAME = "android"; 46 47 // Keep last 24 hours of location app information. 48 private static final long RECENT_TIME_INTERVAL_MILLIS = DateUtils.DAY_IN_MILLIS; 49 50 @VisibleForTesting 51 static final int[] LOCATION_REQUEST_OPS = new int[]{ 52 AppOpsManager.OP_MONITOR_LOCATION, 53 AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, 54 }; 55 @VisibleForTesting 56 static final int[] LOCATION_PERMISSION_OPS = new int[]{ 57 AppOpsManager.OP_FINE_LOCATION, 58 AppOpsManager.OP_COARSE_LOCATION, 59 }; 60 61 private final PackageManager mPackageManager; 62 private final Context mContext; 63 private final IconDrawableFactory mDrawableFactory; 64 RecentLocationApps(Context context)65 public RecentLocationApps(Context context) { 66 mContext = context; 67 mPackageManager = context.getPackageManager(); 68 mDrawableFactory = IconDrawableFactory.newInstance(context); 69 } 70 71 /** 72 * Fills a list of applications which queried location recently within specified time. 73 * Apps are sorted by recency. Apps with more recent location requests are in the front. 74 */ getAppList(boolean showSystemApps)75 public List<Request> getAppList(boolean showSystemApps) { 76 // Retrieve a location usage list from AppOps 77 PackageManager pm = mContext.getPackageManager(); 78 // Retrieve a location usage list from AppOps 79 AppOpsManager aoManager = 80 (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); 81 List<AppOpsManager.PackageOps> appOps = aoManager.getPackagesForOps(LOCATION_REQUEST_OPS); 82 83 final int appOpsCount = appOps != null ? appOps.size() : 0; 84 85 // Process the AppOps list and generate a preference list. 86 ArrayList<Request> requests = new ArrayList<>(appOpsCount); 87 final long now = System.currentTimeMillis(); 88 final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); 89 final List<UserHandle> profiles = um.getUserProfiles(); 90 91 for (int i = 0; i < appOpsCount; ++i) { 92 AppOpsManager.PackageOps ops = appOps.get(i); 93 // Don't show the Android System in the list - it's not actionable for the user. 94 // Also don't show apps belonging to background users except managed users. 95 String packageName = ops.getPackageName(); 96 int uid = ops.getUid(); 97 final UserHandle user = UserHandle.getUserHandleForUid(uid); 98 99 boolean isAndroidOs = 100 (uid == android.os.Process.SYSTEM_UID) && ANDROID_SYSTEM_PACKAGE_NAME.equals( 101 packageName); 102 if (isAndroidOs || !profiles.contains(user)) { 103 continue; 104 } 105 106 // Don't show apps that do not have user sensitive location permissions 107 boolean showApp = true; 108 if (!showSystemApps) { 109 for (int op : LOCATION_PERMISSION_OPS) { 110 final String permission = AppOpsManager.opToPermission(op); 111 final int permissionFlags = pm.getPermissionFlags(permission, packageName, 112 user); 113 if (PermissionChecker.checkPermissionForPreflight(mContext, permission, 114 PermissionChecker.PID_UNKNOWN, uid, packageName) 115 == PermissionChecker.PERMISSION_GRANTED) { 116 if ((permissionFlags 117 & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED) 118 == 0) { 119 showApp = false; 120 break; 121 } 122 } else { 123 if ((permissionFlags 124 & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED) == 0) { 125 showApp = false; 126 break; 127 } 128 } 129 } 130 } 131 if (showApp) { 132 Request request = getRequestFromOps(now, ops); 133 if (request != null) { 134 requests.add(request); 135 } 136 } 137 } 138 return requests; 139 } 140 141 /** 142 * Gets a list of apps that requested for location recently, sorting by recency. 143 * 144 * @param showSystemApps whether includes system apps in the list. 145 * @return the list of apps that recently requested for location. 146 */ getAppListSorted(boolean showSystemApps)147 public List<Request> getAppListSorted(boolean showSystemApps) { 148 List<Request> requests = getAppList(showSystemApps); 149 // Sort the list of Requests by recency. Most recent request first. 150 Collections.sort(requests, Collections.reverseOrder(new Comparator<Request>() { 151 @Override 152 public int compare(Request request1, Request request2) { 153 return Long.compare(request1.requestFinishTime, request2.requestFinishTime); 154 } 155 })); 156 return requests; 157 } 158 159 /** 160 * Creates a Request entry for the given PackageOps. 161 * 162 * This method examines the time interval of the PackageOps first. If the PackageOps is older 163 * than the designated interval, this method ignores the PackageOps object and returns null. 164 * When the PackageOps is fresh enough, this method returns a Request object for the package 165 */ getRequestFromOps(long now, AppOpsManager.PackageOps ops)166 private Request getRequestFromOps(long now, 167 AppOpsManager.PackageOps ops) { 168 String packageName = ops.getPackageName(); 169 List<AppOpsManager.OpEntry> entries = ops.getOps(); 170 boolean highBattery = false; 171 boolean normalBattery = false; 172 long locationRequestFinishTime = 0L; 173 // Earliest time for a location request to end and still be shown in list. 174 long recentLocationCutoffTime = now - RECENT_TIME_INTERVAL_MILLIS; 175 for (AppOpsManager.OpEntry entry : entries) { 176 if (entry.isRunning() || entry.getTime() >= recentLocationCutoffTime) { 177 locationRequestFinishTime = entry.getTime() + entry.getDuration(); 178 switch (entry.getOp()) { 179 case AppOpsManager.OP_MONITOR_LOCATION: 180 normalBattery = true; 181 break; 182 case AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION: 183 highBattery = true; 184 break; 185 default: 186 break; 187 } 188 } 189 } 190 191 if (!highBattery && !normalBattery) { 192 if (Log.isLoggable(TAG, Log.VERBOSE)) { 193 Log.v(TAG, packageName + " hadn't used location within the time interval."); 194 } 195 return null; 196 } 197 198 // The package is fresh enough, continue. 199 int uid = ops.getUid(); 200 int userId = UserHandle.getUserId(uid); 201 202 Request request = null; 203 try { 204 ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser( 205 packageName, PackageManager.GET_META_DATA, userId); 206 if (appInfo == null) { 207 Log.w(TAG, "Null application info retrieved for package " + packageName 208 + ", userId " + userId); 209 return null; 210 } 211 212 final UserHandle userHandle = new UserHandle(userId); 213 Drawable icon = mDrawableFactory.getBadgedIcon(appInfo, userId); 214 CharSequence appLabel = mPackageManager.getApplicationLabel(appInfo); 215 CharSequence badgedAppLabel = mPackageManager.getUserBadgedLabel(appLabel, userHandle); 216 if (appLabel.toString().contentEquals(badgedAppLabel)) { 217 // If badged label is not different from original then no need for it as 218 // a separate content description. 219 badgedAppLabel = null; 220 } 221 request = new Request(packageName, userHandle, icon, appLabel, highBattery, 222 badgedAppLabel, locationRequestFinishTime); 223 } catch (NameNotFoundException e) { 224 Log.w(TAG, "package name not found for " + packageName + ", userId " + userId); 225 } 226 return request; 227 } 228 229 public static class Request { 230 public final String packageName; 231 public final UserHandle userHandle; 232 public final Drawable icon; 233 public final CharSequence label; 234 public final boolean isHighBattery; 235 public final CharSequence contentDescription; 236 public final long requestFinishTime; 237 Request(String packageName, UserHandle userHandle, Drawable icon, CharSequence label, boolean isHighBattery, CharSequence contentDescription, long requestFinishTime)238 public Request(String packageName, UserHandle userHandle, Drawable icon, 239 CharSequence label, boolean isHighBattery, CharSequence contentDescription, 240 long requestFinishTime) { 241 this.packageName = packageName; 242 this.userHandle = userHandle; 243 this.icon = icon; 244 this.label = label; 245 this.isHighBattery = isHighBattery; 246 this.contentDescription = contentDescription; 247 this.requestFinishTime = requestFinishTime; 248 } 249 } 250 } 251