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             String packageName = ops.getPackageName();
94             int uid = ops.getUid();
95             final UserHandle user = UserHandle.getUserHandleForUid(uid);
96 
97             // Don't show apps belonging to background users except managed users.
98             if (!profiles.contains(user)) {
99                 continue;
100             }
101 
102             // Don't show apps that do not have user sensitive location permissions
103             boolean showApp = true;
104             if (!showSystemApps) {
105                 for (int op : LOCATION_PERMISSION_OPS) {
106                     final String permission = AppOpsManager.opToPermission(op);
107                     final int permissionFlags = pm.getPermissionFlags(permission, packageName,
108                             user);
109                     if (PermissionChecker.checkPermission(mContext, permission, -1, uid,
110                             packageName)
111                             == PermissionChecker.PERMISSION_GRANTED) {
112                         if ((permissionFlags
113                                 & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED)
114                                 == 0) {
115                             showApp = false;
116                             break;
117                         }
118                     } else {
119                         if ((permissionFlags
120                                 & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED) == 0) {
121                             showApp = false;
122                             break;
123                         }
124                     }
125                 }
126             }
127             if (showApp) {
128                 Request request = getRequestFromOps(now, ops);
129                 if (request != null) {
130                     requests.add(request);
131                 }
132             }
133         }
134         return requests;
135     }
136 
137     /**
138      * Gets a list of apps that requested for location recently, sorting by recency.
139      *
140      * @param showSystemApps whether includes system apps in the list.
141      * @return the list of apps that recently requested for location.
142      */
getAppListSorted(boolean showSystemApps)143     public List<Request> getAppListSorted(boolean showSystemApps) {
144         List<Request> requests = getAppList(showSystemApps);
145         // Sort the list of Requests by recency. Most recent request first.
146         Collections.sort(requests, Collections.reverseOrder(new Comparator<Request>() {
147             @Override
148             public int compare(Request request1, Request request2) {
149                 return Long.compare(request1.requestFinishTime, request2.requestFinishTime);
150             }
151         }));
152         return requests;
153     }
154 
155     /**
156      * Creates a Request entry for the given PackageOps.
157      *
158      * This method examines the time interval of the PackageOps first. If the PackageOps is older
159      * than the designated interval, this method ignores the PackageOps object and returns null.
160      * When the PackageOps is fresh enough, this method returns a Request object for the package
161      */
getRequestFromOps(long now, AppOpsManager.PackageOps ops)162     private Request getRequestFromOps(long now,
163             AppOpsManager.PackageOps ops) {
164         String packageName = ops.getPackageName();
165         List<AppOpsManager.OpEntry> entries = ops.getOps();
166         boolean highBattery = false;
167         boolean normalBattery = false;
168         long locationRequestFinishTime = 0L;
169         // Earliest time for a location request to end and still be shown in list.
170         long recentLocationCutoffTime = now - RECENT_TIME_INTERVAL_MILLIS;
171         for (AppOpsManager.OpEntry entry : entries) {
172             if (entry.isRunning() || entry.getTime() >= recentLocationCutoffTime) {
173                 locationRequestFinishTime = entry.getTime() + entry.getDuration();
174                 switch (entry.getOp()) {
175                     case AppOpsManager.OP_MONITOR_LOCATION:
176                         normalBattery = true;
177                         break;
178                     case AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION:
179                         highBattery = true;
180                         break;
181                     default:
182                         break;
183                 }
184             }
185         }
186 
187         if (!highBattery && !normalBattery) {
188             if (Log.isLoggable(TAG, Log.VERBOSE)) {
189                 Log.v(TAG, packageName + " hadn't used location within the time interval.");
190             }
191             return null;
192         }
193 
194         // The package is fresh enough, continue.
195         int uid = ops.getUid();
196         int userId = UserHandle.getUserId(uid);
197 
198         Request request = null;
199         try {
200             ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser(
201                     packageName, PackageManager.GET_META_DATA, userId);
202             if (appInfo == null) {
203                 Log.w(TAG, "Null application info retrieved for package " + packageName
204                         + ", userId " + userId);
205                 return null;
206             }
207 
208             final UserHandle userHandle = new UserHandle(userId);
209             Drawable icon = mDrawableFactory.getBadgedIcon(appInfo, userId);
210             CharSequence appLabel = mPackageManager.getApplicationLabel(appInfo);
211             CharSequence badgedAppLabel = mPackageManager.getUserBadgedLabel(appLabel, userHandle);
212             if (appLabel.toString().contentEquals(badgedAppLabel)) {
213                 // If badged label is not different from original then no need for it as
214                 // a separate content description.
215                 badgedAppLabel = null;
216             }
217             request = new Request(packageName, userHandle, icon, appLabel, highBattery,
218                     badgedAppLabel, locationRequestFinishTime);
219         } catch (NameNotFoundException e) {
220             Log.w(TAG, "package name not found for " + packageName + ", userId " + userId);
221         }
222         return request;
223     }
224 
225     public static class Request {
226         public final String packageName;
227         public final UserHandle userHandle;
228         public final Drawable icon;
229         public final CharSequence label;
230         public final boolean isHighBattery;
231         public final CharSequence contentDescription;
232         public final long requestFinishTime;
233 
Request(String packageName, UserHandle userHandle, Drawable icon, CharSequence label, boolean isHighBattery, CharSequence contentDescription, long requestFinishTime)234         private Request(String packageName, UserHandle userHandle, Drawable icon,
235                 CharSequence label, boolean isHighBattery, CharSequence contentDescription,
236                 long requestFinishTime) {
237             this.packageName = packageName;
238             this.userHandle = userHandle;
239             this.icon = icon;
240             this.label = label;
241             this.isHighBattery = isHighBattery;
242             this.contentDescription = contentDescription;
243             this.requestFinishTime = requestFinishTime;
244         }
245     }
246 }
247