• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.permissioncontroller.permission.service;
18 
19 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
20 import static android.Manifest.permission_group.LOCATION;
21 import static android.app.AppOpsManager.OPSTR_FINE_LOCATION;
22 import static android.app.NotificationManager.IMPORTANCE_LOW;
23 import static android.app.PendingIntent.FLAG_ONE_SHOT;
24 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
25 import static android.app.PendingIntent.getBroadcast;
26 import static android.app.job.JobScheduler.RESULT_SUCCESS;
27 import static android.content.Context.MODE_PRIVATE;
28 import static android.content.Intent.ACTION_MANAGE_APP_PERMISSION;
29 import static android.content.Intent.EXTRA_PACKAGE_NAME;
30 import static android.content.Intent.EXTRA_PERMISSION_GROUP_NAME;
31 import static android.content.Intent.EXTRA_UID;
32 import static android.content.Intent.EXTRA_USER;
33 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
34 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
35 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
36 import static android.content.pm.PackageManager.GET_PERMISSIONS;
37 import static android.graphics.Bitmap.Config.ARGB_8888;
38 import static android.graphics.Bitmap.createBitmap;
39 import static android.os.UserHandle.getUserHandleForUid;
40 import static android.os.UserHandle.myUserId;
41 import static android.provider.Settings.Secure.LOCATION_ACCESS_CHECK_DELAY_MILLIS;
42 import static android.provider.Settings.Secure.LOCATION_ACCESS_CHECK_INTERVAL_MILLIS;
43 
44 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID;
45 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID;
46 import static com.android.permissioncontroller.Constants.KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN;
47 import static com.android.permissioncontroller.Constants.KEY_LOCATION_ACCESS_CHECK_ENABLED_TIME;
48 import static com.android.permissioncontroller.Constants.LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE;
49 import static com.android.permissioncontroller.Constants.LOCATION_ACCESS_CHECK_JOB_ID;
50 import static com.android.permissioncontroller.Constants.LOCATION_ACCESS_CHECK_NOTIFICATION_ID;
51 import static com.android.permissioncontroller.Constants.PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID;
52 import static com.android.permissioncontroller.Constants.PERMISSION_REMINDER_CHANNEL_ID;
53 import static com.android.permissioncontroller.Constants.PREFERENCES_FILE;
54 import static com.android.permissioncontroller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION;
55 import static com.android.permissioncontroller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_CLICKED;
56 import static com.android.permissioncontroller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_DECLINED;
57 import static com.android.permissioncontroller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_PRESENTED;
58 import static com.android.permissioncontroller.permission.utils.Utils.OS_PKG;
59 import static com.android.permissioncontroller.permission.utils.Utils.getParcelableExtraSafe;
60 import static com.android.permissioncontroller.permission.utils.Utils.getParentUserContext;
61 import static com.android.permissioncontroller.permission.utils.Utils.getStringExtraSafe;
62 import static com.android.permissioncontroller.permission.utils.Utils.getSystemServiceSafe;
63 
64 import static java.lang.System.currentTimeMillis;
65 import static java.util.concurrent.TimeUnit.DAYS;
66 
67 import android.app.AppOpsManager;
68 import android.app.AppOpsManager.OpEntry;
69 import android.app.AppOpsManager.PackageOps;
70 import android.app.Notification;
71 import android.app.NotificationChannel;
72 import android.app.NotificationManager;
73 import android.app.job.JobInfo;
74 import android.app.job.JobParameters;
75 import android.app.job.JobScheduler;
76 import android.app.job.JobService;
77 import android.content.BroadcastReceiver;
78 import android.content.ComponentName;
79 import android.content.ContentResolver;
80 import android.content.Context;
81 import android.content.Intent;
82 import android.content.SharedPreferences;
83 import android.content.pm.PackageInfo;
84 import android.content.pm.PackageManager;
85 import android.graphics.Bitmap;
86 import android.graphics.Canvas;
87 import android.graphics.drawable.Drawable;
88 import android.location.LocationManager;
89 import android.net.Uri;
90 import android.os.AsyncTask;
91 import android.os.Bundle;
92 import android.os.UserHandle;
93 import android.os.UserManager;
94 import android.provider.Settings;
95 import android.service.notification.StatusBarNotification;
96 import android.util.ArraySet;
97 import android.util.Log;
98 
99 import androidx.annotation.NonNull;
100 import androidx.annotation.Nullable;
101 import androidx.annotation.WorkerThread;
102 import androidx.core.util.Preconditions;
103 
104 import com.android.permissioncontroller.PermissionControllerStatsLog;
105 import com.android.permissioncontroller.R;
106 import com.android.permissioncontroller.permission.model.AppPermissionGroup;
107 import com.android.permissioncontroller.permission.utils.Utils;
108 
109 import java.io.BufferedReader;
110 import java.io.BufferedWriter;
111 import java.io.FileNotFoundException;
112 import java.io.IOException;
113 import java.io.InputStreamReader;
114 import java.io.OutputStreamWriter;
115 import java.util.ArrayList;
116 import java.util.List;
117 import java.util.Objects;
118 import java.util.Random;
119 import java.util.function.BooleanSupplier;
120 
121 /**
122  * Show notification that double-guesses the user if she/he really wants to grant fine background
123  * location access to an app.
124  *
125  * <p>A notification is scheduled after the background permission access is granted via
126  * {@link #checkLocationAccessSoon()} or periodically.
127  *
128  * <p>We rate limit the number of notification we show and only ever show one notification at a
129  * time. Further we only shown notifications if the app has actually accessed the fine location
130  * in the background.
131  *
132  * <p>As there are many cases why a notification should not been shown, we always schedule a
133  * {@link #addLocationNotificationIfNeeded check} which then might add a notification.
134  */
135 public class LocationAccessCheck {
136     private static final String LOG_TAG = LocationAccessCheck.class.getSimpleName();
137     private static final boolean DEBUG = false;
138 
139     /** Lock required for all methods called {@code ...Locked} */
140     private static final Object sLock = new Object();
141 
142     private final Random mRandom = new Random();
143 
144     private final @NonNull Context mContext;
145     private final @NonNull JobScheduler mJobScheduler;
146     private final @NonNull ContentResolver mContentResolver;
147     private final @NonNull AppOpsManager mAppOpsManager;
148     private final @NonNull PackageManager mPackageManager;
149     private final @NonNull UserManager mUserManager;
150     private final @NonNull SharedPreferences mSharedPrefs;
151 
152     /** If the current long running operation should be canceled */
153     private final @Nullable BooleanSupplier mShouldCancel;
154 
155     /**
156      * Get time in between two periodic checks.
157      *
158      * <p>Default: 1 day
159      *
160      * @return The time in between check in milliseconds
161      */
getPeriodicCheckIntervalMillis()162     private long getPeriodicCheckIntervalMillis() {
163         return Settings.Secure.getLong(mContentResolver,
164                 LOCATION_ACCESS_CHECK_INTERVAL_MILLIS, DAYS.toMillis(1));
165     }
166 
167     /**
168      * Flexibility of the periodic check.
169      *
170      * <p>10% of {@link #getPeriodicCheckIntervalMillis()}
171      *
172      * @return The flexibility of the periodic check in milliseconds
173      */
getFlexForPeriodicCheckMillis()174     private long getFlexForPeriodicCheckMillis() {
175         return getPeriodicCheckIntervalMillis() / 10;
176     }
177 
178     /**
179      * Get the delay in between granting a permission and the follow up check.
180      *
181      * <p>Default: 1 day
182      *
183      * @return The delay in milliseconds
184      */
getDelayMillis()185     private long getDelayMillis() {
186         return Settings.Secure.getLong(mContentResolver,
187                 LOCATION_ACCESS_CHECK_DELAY_MILLIS, DAYS.toMillis(1));
188     }
189 
190     /**
191      * Minimum time in between showing two notifications.
192      *
193      * <p>This is just small enough so that the periodic check can always show a notification.
194      *
195      * @return The minimum time in milliseconds
196      */
getInBetweenNotificationsMillis()197     private long getInBetweenNotificationsMillis() {
198         return getPeriodicCheckIntervalMillis() - (long) (getFlexForPeriodicCheckMillis() * 2.1);
199     }
200 
201     /**
202      * Load the list of {@link UserPackage packages} we already shown a notification for.
203      *
204      * @return The list of packages we already shown a notification for.
205      */
loadAlreadyNotifiedPackagesLocked()206     private @NonNull ArraySet<UserPackage> loadAlreadyNotifiedPackagesLocked() {
207         try (BufferedReader reader = new BufferedReader(new InputStreamReader(
208                 mContext.openFileInput(LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE)))) {
209             ArraySet<UserPackage> packages = new ArraySet<>();
210 
211             /*
212              * The format of the file is <package> <serial of user>, e.g.
213              *
214              * com.one.package 5630633845
215              * com.two.package 5630633853
216              * com.three.package 5630633853
217              */
218             while (true) {
219                 String line = reader.readLine();
220                 if (line == null) {
221                     break;
222                 }
223 
224                 String[] lineComponents = line.split(" ");
225                 String pkg = lineComponents[0];
226                 UserHandle user = mUserManager.getUserForSerialNumber(
227                         Long.valueOf(lineComponents[1]));
228 
229                 if (user != null) {
230                     packages.add(new UserPackage(mContext, pkg, user));
231                 } else {
232                     Log.i(LOG_TAG, "Not restoring state \"" + line + "\" as user is unknown");
233                 }
234             }
235 
236             return packages;
237         } catch (FileNotFoundException ignored) {
238             return new ArraySet<>();
239         } catch (Exception e) {
240             Log.w(LOG_TAG, "Could not read " + LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE, e);
241             return new ArraySet<>();
242         }
243     }
244 
245     /**
246      * Safe the list of {@link UserPackage packages} we have already shown a notification for.
247      *
248      * @param packages The list of packages we already shown a notification for.
249      */
safeAlreadyNotifiedPackagesLocked(@onNull ArraySet<UserPackage> packages)250     private void safeAlreadyNotifiedPackagesLocked(@NonNull ArraySet<UserPackage> packages) {
251         try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
252                 mContext.openFileOutput(LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE,
253                         MODE_PRIVATE)))) {
254             /*
255              * The format of the file is <package> <serial of user>, e.g.
256              *
257              * com.one.package 5630633845
258              * com.two.package 5630633853
259              * com.three.package 5630633853
260              */
261             int numPkgs = packages.size();
262             for (int i = 0; i < numPkgs; i++) {
263                 UserPackage userPkg = packages.valueAt(i);
264 
265                 writer.append(userPkg.pkg);
266                 writer.append(' ');
267                 writer.append(
268                         Long.valueOf(mUserManager.getSerialNumberForUser(userPkg.user)).toString());
269                 writer.newLine();
270             }
271         } catch (IOException e) {
272             Log.e(LOG_TAG, "Could not write " + LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE, e);
273         }
274     }
275 
276     /**
277      * Remember that we showed a notification for a {@link UserPackage}
278      *
279      * @param pkg The package we notified for
280      * @param user The user we notified for
281      */
markAsNotified(@onNull String pkg, @NonNull UserHandle user)282     private void markAsNotified(@NonNull String pkg, @NonNull UserHandle user) {
283         synchronized (sLock) {
284             ArraySet<UserPackage> alreadyNotifiedPackages = loadAlreadyNotifiedPackagesLocked();
285             alreadyNotifiedPackages.add(new UserPackage(mContext, pkg, user));
286             safeAlreadyNotifiedPackagesLocked(alreadyNotifiedPackages);
287         }
288     }
289 
290     /**
291      * Create the channel the location access notifications should be posted to.
292      *
293      * @param user The user to create the channel for
294      */
createPermissionReminderChannel(@onNull UserHandle user)295     private void createPermissionReminderChannel(@NonNull UserHandle user) {
296         NotificationManager notificationManager = getSystemServiceSafe(mContext,
297                 NotificationManager.class, user);
298 
299         NotificationChannel permissionReminderChannel = new NotificationChannel(
300                 PERMISSION_REMINDER_CHANNEL_ID, mContext.getString(R.string.permission_reminders),
301                 IMPORTANCE_LOW);
302         notificationManager.createNotificationChannel(permissionReminderChannel);
303     }
304 
305     /**
306      * If {@link #mShouldCancel} throw an {@link InterruptedException}.
307      */
throwInterruptedExceptionIfTaskIsCanceled()308     private void throwInterruptedExceptionIfTaskIsCanceled() throws InterruptedException {
309         if (mShouldCancel != null && mShouldCancel.getAsBoolean()) {
310             throw new InterruptedException();
311         }
312     }
313 
314     /**
315      * Create a new {@link LocationAccessCheck} object.
316      *
317      * @param context Used to resolve managers
318      * @param shouldCancel If supplied, can be used to interrupt long running operations
319      */
LocationAccessCheck(@onNull Context context, @Nullable BooleanSupplier shouldCancel)320     public LocationAccessCheck(@NonNull Context context, @Nullable BooleanSupplier shouldCancel) {
321         mContext = getParentUserContext(context);
322 
323         mJobScheduler = getSystemServiceSafe(mContext, JobScheduler.class);
324         mAppOpsManager = getSystemServiceSafe(mContext, AppOpsManager.class);
325         mPackageManager = mContext.getPackageManager();
326         mUserManager = getSystemServiceSafe(mContext, UserManager.class);
327         mSharedPrefs = mContext.getSharedPreferences(PREFERENCES_FILE, MODE_PRIVATE);
328         mContentResolver = mContext.getContentResolver();
329 
330         mShouldCancel = shouldCancel;
331     }
332 
333     /**
334      * Check if a location access notification should be shown and then add it.
335      *
336      * <p>Always run async inside a
337      * {@link LocationAccessCheckJobService.AddLocationNotificationIfNeededTask}.
338      */
339     @WorkerThread
addLocationNotificationIfNeeded(@onNull JobParameters params, @NonNull LocationAccessCheckJobService service)340     private void addLocationNotificationIfNeeded(@NonNull JobParameters params,
341             @NonNull LocationAccessCheckJobService service) {
342         if (!checkLocationAccessCheckEnabledAndUpdateEnabledTime()) {
343             service.jobFinished(params, false);
344             return;
345         }
346 
347         synchronized (sLock) {
348             try {
349                 if (currentTimeMillis() - mSharedPrefs.getLong(
350                         KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN, 0)
351                         < getInBetweenNotificationsMillis()) {
352                     service.jobFinished(params, false);
353                     return;
354                 }
355 
356                 if (getCurrentlyShownNotificationLocked() != null) {
357                     service.jobFinished(params, false);
358                     return;
359                 }
360 
361                 addLocationNotificationIfNeeded(mAppOpsManager.getPackagesForOps(
362                         new String[]{OPSTR_FINE_LOCATION}));
363                 service.jobFinished(params, false);
364             } catch (Exception e) {
365                 Log.e(LOG_TAG, "Could not check for location access", e);
366                 service.jobFinished(params, true);
367             } finally {
368                 synchronized (sLock) {
369                     service.mAddLocationNotificationIfNeededTask = null;
370                 }
371             }
372         }
373     }
374 
addLocationNotificationIfNeeded(@onNull List<PackageOps> ops)375     private void addLocationNotificationIfNeeded(@NonNull List<PackageOps> ops)
376             throws InterruptedException {
377         synchronized (sLock) {
378             List<UserPackage> packages = getLocationUsersWithNoNotificationYetLocked(ops);
379 
380             // Get a random package and resolve package info
381             PackageInfo pkgInfo = null;
382             while (pkgInfo == null) {
383                 throwInterruptedExceptionIfTaskIsCanceled();
384 
385                 if (packages.isEmpty()) {
386                     return;
387                 }
388 
389                 UserPackage packageToNotifyFor = null;
390 
391                 // Prefer to show notification for location controller extra package
392                 int numPkgs = packages.size();
393                 for (int i = 0; i < numPkgs; i++) {
394                     UserPackage pkg = packages.get(i);
395 
396                     LocationManager locationManager = getSystemServiceSafe(mContext,
397                             LocationManager.class, pkg.user);
398                     if (locationManager.isExtraLocationControllerPackageEnabled() && pkg.pkg.equals(
399                             locationManager.getExtraLocationControllerPackage())) {
400                         packageToNotifyFor = pkg;
401                         break;
402                     }
403                 }
404 
405                 if (packageToNotifyFor == null) {
406                     packageToNotifyFor = packages.get(mRandom.nextInt(packages.size()));
407                 }
408 
409                 try {
410                     pkgInfo = packageToNotifyFor.getPackageInfo();
411                 } catch (PackageManager.NameNotFoundException e) {
412                     packages.remove(packageToNotifyFor);
413                 }
414             }
415 
416             createPermissionReminderChannel(getUserHandleForUid(pkgInfo.applicationInfo.uid));
417             createNotificationForLocationUser(pkgInfo);
418         }
419     }
420 
421     /**
422      * Get the {@link UserPackage packages} which accessed the location but we have not yet shown
423      * a notification for.
424      *
425      * <p>This also ignores all packages that are excepted from the notification.
426      *
427      * @return The packages we need to show a notification for
428      *
429      * @throws InterruptedException If {@link #mShouldCancel}
430      */
getLocationUsersWithNoNotificationYetLocked( @onNull List<PackageOps> allOps)431     private @NonNull List<UserPackage> getLocationUsersWithNoNotificationYetLocked(
432             @NonNull List<PackageOps> allOps) throws InterruptedException {
433         List<UserPackage> pkgsWithLocationAccess = new ArrayList<>();
434         List<UserHandle> profiles = mUserManager.getUserProfiles();
435 
436         LocationManager lm = mContext.getSystemService(LocationManager.class);
437 
438         int numPkgs = allOps.size();
439         for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) {
440             PackageOps packageOps = allOps.get(pkgNum);
441 
442             String pkg = packageOps.getPackageName();
443             if (pkg.equals(OS_PKG) || lm.isProviderPackage(pkg)) {
444                 continue;
445             }
446 
447             UserHandle user = getUserHandleForUid(packageOps.getUid());
448             // Do not handle apps that belong to a different profile user group
449             if (!profiles.contains(user)) {
450                 continue;
451             }
452 
453             UserPackage userPkg = new UserPackage(mContext, pkg, user);
454 
455             AppPermissionGroup bgLocationGroup = userPkg.getBackgroundLocationGroup();
456             // Do not show notification that do not request the background permission anymore
457             if (bgLocationGroup == null) {
458                 continue;
459             }
460 
461             // Do not show notification that do not currently have the background permission
462             // granted
463             if (!bgLocationGroup.areRuntimePermissionsGranted()) {
464                 continue;
465             }
466 
467             // Do not show notification for permissions that are not user sensitive
468             if (!bgLocationGroup.isUserSensitive()) {
469                 continue;
470             }
471 
472             // Never show notification for pregranted permissions as warning the user via the
473             // notification and then warning the user again when revoking the permission is
474             // confusing
475             if (userPkg.getLocationGroup().hasGrantedByDefaultPermission()
476                     && bgLocationGroup.hasGrantedByDefaultPermission()) {
477                 continue;
478             }
479 
480             int numOps = packageOps.getOps().size();
481             for (int opNum = 0; opNum < numOps; opNum++) {
482                 OpEntry entry = packageOps.getOps().get(opNum);
483 
484                 // To protect against OEM apps that accidentally blame app ops on other packages
485                 // since they can hold the privileged UPDATE_APP_OPS_STATS permission for location
486                 // access in the background we trust only the OS and the location providers. Note
487                 // that this mitigation only handles usage of AppOpsManager#noteProxyOp and not
488                 // direct usage of AppOpsManager#noteOp, i.e. handles bad blaming and not bad
489                 // attribution.
490                 String proxyPackageName = entry.getProxyPackageName();
491                 if (proxyPackageName != null && !proxyPackageName.equals(OS_PKG)
492                         && !lm.isProviderPackage(proxyPackageName)) {
493                     continue;
494                 }
495 
496                 // We show only bg accesses since the location access check feature was enabled
497                 // to handle cases where the feature is remotely toggled since we don't want to
498                 // notify for accesses before the feature was turned on.
499                 long featureEnabledTime = getLocationAccessCheckEnabledTime();
500                 if (featureEnabledTime >= 0 && entry.getLastAccessBackgroundTime(
501                         AppOpsManager.OP_FLAGS_ALL_TRUSTED) >= featureEnabledTime) {
502                     pkgsWithLocationAccess.add(userPkg);
503                     break;
504                 }
505             }
506         }
507 
508         ArraySet<UserPackage> alreadyNotifiedPkgs = loadAlreadyNotifiedPackagesLocked();
509         throwInterruptedExceptionIfTaskIsCanceled();
510 
511         resetAlreadyNotifiedPackagesWithoutPermissionLocked(alreadyNotifiedPkgs);
512 
513         pkgsWithLocationAccess.removeAll(alreadyNotifiedPkgs);
514         return pkgsWithLocationAccess;
515     }
516 
517     /**
518      * Checks whether the location access check feature is enabled and updates the
519      * time when the feature was first enabled. If the feature is enabled and no
520      * enabled time persisted we persist the current time as the enabled time. If
521      * the feature is disabled and an enabled time is persisted we delete the
522      * persisted time.
523      *
524      * @return Whether the location access feature is enabled.
525      */
checkLocationAccessCheckEnabledAndUpdateEnabledTime()526     private boolean checkLocationAccessCheckEnabledAndUpdateEnabledTime() {
527         final long enabledTime = getLocationAccessCheckEnabledTime();
528         if (Utils.isLocationAccessCheckEnabled()) {
529             if (enabledTime <= 0) {
530                 mSharedPrefs.edit().putLong(KEY_LOCATION_ACCESS_CHECK_ENABLED_TIME,
531                         currentTimeMillis()).commit();
532             }
533             return true;
534         } else {
535             if (enabledTime > 0) {
536                 mSharedPrefs.edit().remove(KEY_LOCATION_ACCESS_CHECK_ENABLED_TIME)
537                         .commit();
538             }
539             return false;
540         }
541     }
542 
543     /**
544      * @return The time the location access check was enabled, or 0 if not enabled.
545      */
getLocationAccessCheckEnabledTime()546     private long getLocationAccessCheckEnabledTime() {
547         return mSharedPrefs.getLong(KEY_LOCATION_ACCESS_CHECK_ENABLED_TIME, 0);
548     }
549 
550     /**
551      * Create a notification reminding the user that a package used the location. From this
552      * notification the user can directly go to the screen that allows to change the permission.
553      *
554      * @param pkg The {@link PackageInfo} for the package to to be changed
555      */
createNotificationForLocationUser(@onNull PackageInfo pkg)556     private void createNotificationForLocationUser(@NonNull PackageInfo pkg) {
557         CharSequence pkgLabel = mPackageManager.getApplicationLabel(pkg.applicationInfo);
558         Drawable pkgIcon = mPackageManager.getApplicationIcon(pkg.applicationInfo);
559         Bitmap pkgIconBmp = createBitmap(pkgIcon.getIntrinsicWidth(), pkgIcon.getIntrinsicHeight(),
560                 ARGB_8888);
561         Canvas canvas = new Canvas(pkgIconBmp);
562         pkgIcon.setBounds(0, 0, pkgIcon.getIntrinsicWidth(), pkgIcon.getIntrinsicHeight());
563         pkgIcon.draw(canvas);
564 
565         String pkgName = pkg.packageName;
566         UserHandle user = getUserHandleForUid(pkg.applicationInfo.uid);
567 
568         NotificationManager notificationManager = getSystemServiceSafe(mContext,
569                 NotificationManager.class, user);
570 
571         long sessionId = INVALID_SESSION_ID;
572         while (sessionId == INVALID_SESSION_ID) {
573             sessionId = new Random().nextLong();
574         }
575 
576         Intent deleteIntent = new Intent(mContext, NotificationDeleteHandler.class);
577         deleteIntent.putExtra(EXTRA_PACKAGE_NAME, pkgName);
578         deleteIntent.putExtra(EXTRA_SESSION_ID, sessionId);
579         deleteIntent.putExtra(EXTRA_UID, pkg.applicationInfo.uid);
580         deleteIntent.putExtra(EXTRA_USER, user);
581         deleteIntent.setFlags(FLAG_RECEIVER_FOREGROUND);
582 
583         Intent clickIntent = new Intent(mContext, NotificationClickHandler.class);
584         clickIntent.putExtra(EXTRA_PACKAGE_NAME, pkgName);
585         clickIntent.putExtra(EXTRA_SESSION_ID, sessionId);
586         clickIntent.putExtra(EXTRA_UID, pkg.applicationInfo.uid);
587         clickIntent.putExtra(EXTRA_USER, user);
588         clickIntent.setFlags(FLAG_RECEIVER_FOREGROUND);
589 
590         CharSequence appName = Utils.getSettingsLabelForNotifications(mPackageManager);
591 
592         Notification.Builder b = (new Notification.Builder(mContext,
593                 PERMISSION_REMINDER_CHANNEL_ID))
594                 .setLocalOnly(true)
595                 .setContentTitle(mContext.getString(
596                         R.string.background_location_access_reminder_notification_title, pkgLabel))
597                 .setContentText(mContext.getString(
598                         R.string.background_location_access_reminder_notification_content))
599                 .setStyle(new Notification.BigTextStyle().bigText(mContext.getString(
600                         R.string.background_location_access_reminder_notification_content)))
601                 .setSmallIcon(R.drawable.ic_pin_drop)
602                 .setLargeIcon(pkgIconBmp)
603                 .setColor(mContext.getColor(android.R.color.system_notification_accent_color))
604                 .setAutoCancel(true)
605                 .setDeleteIntent(getBroadcast(mContext, 0, deleteIntent,
606                         FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT))
607                 .setContentIntent(getBroadcast(mContext, 0, clickIntent,
608                         FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT));
609 
610         if (appName != null) {
611             Bundle extras = new Bundle();
612             extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, appName.toString());
613             b.addExtras(extras);
614         }
615 
616         notificationManager.notify(pkgName, LOCATION_ACCESS_CHECK_NOTIFICATION_ID, b.build());
617 
618         if (DEBUG) Log.i(LOG_TAG, "Notified " + pkgName);
619 
620         PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId,
621                 pkg.applicationInfo.uid, pkgName,
622                 LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_PRESENTED);
623         Log.v(LOG_TAG, "Location access check notification shown with sessionId=" + sessionId + ""
624                 + " uid=" + pkg.applicationInfo.uid + " pkgName=" + pkgName);
625 
626         mSharedPrefs.edit().putLong(KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN,
627                 currentTimeMillis()).apply();
628     }
629 
630     /**
631      * Get currently shown notification. We only ever show one notification per profile group.
632      *
633      * @return The notification or {@code null} if no notification is currently shown
634      */
getCurrentlyShownNotificationLocked()635     private @Nullable StatusBarNotification getCurrentlyShownNotificationLocked() {
636         List<UserHandle> profiles = mUserManager.getUserProfiles();
637 
638         int numProfiles = profiles.size();
639         for (int profileNum = 0; profileNum < numProfiles; profileNum++) {
640             NotificationManager notificationManager;
641             try {
642                 notificationManager = getSystemServiceSafe(mContext, NotificationManager.class,
643                         profiles.get(profileNum));
644             } catch (IllegalStateException e) {
645                 continue;
646             }
647 
648             StatusBarNotification[] notifications = notificationManager.getActiveNotifications();
649 
650             int numNotifications = notifications.length;
651             for (int notificationNum = 0; notificationNum < numNotifications; notificationNum++) {
652                 StatusBarNotification notification = notifications[notificationNum];
653 
654                 if (notification.getId() == LOCATION_ACCESS_CHECK_NOTIFICATION_ID) {
655                     return notification;
656                 }
657             }
658         }
659 
660         return null;
661     }
662 
663     /**
664      * Go through the list of packages we already shown a notification for and remove those that do
665      * not request fine background location access.
666      *
667      * @param alreadyNotifiedPkgs The packages we already shown a notification for. This paramter is
668      *                            modified inside of this method.
669      *
670      * @throws InterruptedException If {@link #mShouldCancel}
671      */
resetAlreadyNotifiedPackagesWithoutPermissionLocked( @onNull ArraySet<UserPackage> alreadyNotifiedPkgs)672     private void resetAlreadyNotifiedPackagesWithoutPermissionLocked(
673             @NonNull ArraySet<UserPackage> alreadyNotifiedPkgs) throws InterruptedException {
674         ArrayList<UserPackage> packagesToRemove = new ArrayList<>();
675 
676         for (UserPackage userPkg : alreadyNotifiedPkgs) {
677             throwInterruptedExceptionIfTaskIsCanceled();
678 
679             AppPermissionGroup bgLocationGroup = userPkg.getBackgroundLocationGroup();
680             if (bgLocationGroup == null || !bgLocationGroup.areRuntimePermissionsGranted()) {
681                 packagesToRemove.add(userPkg);
682             }
683         }
684 
685         if (!packagesToRemove.isEmpty()) {
686             alreadyNotifiedPkgs.removeAll(packagesToRemove);
687             safeAlreadyNotifiedPackagesLocked(alreadyNotifiedPkgs);
688             throwInterruptedExceptionIfTaskIsCanceled();
689         }
690     }
691 
692     /**
693      * Remove all persisted state for a package.
694      *
695      * @param pkg name of package
696      * @param user user the package belongs to
697      */
forgetAboutPackage(@onNull String pkg, @NonNull UserHandle user)698     private void forgetAboutPackage(@NonNull String pkg, @NonNull UserHandle user) {
699         synchronized (sLock) {
700             StatusBarNotification notification = getCurrentlyShownNotificationLocked();
701             if (notification != null && notification.getUser().equals(user)
702                     && notification.getTag().equals(pkg)) {
703                 getSystemServiceSafe(mContext, NotificationManager.class, user).cancel(
704                         pkg, LOCATION_ACCESS_CHECK_NOTIFICATION_ID);
705             }
706 
707             ArraySet<UserPackage> packages = loadAlreadyNotifiedPackagesLocked();
708             packages.remove(new UserPackage(mContext, pkg, user));
709             safeAlreadyNotifiedPackagesLocked(packages);
710         }
711     }
712 
713     /**
714      * After a small delay schedule a check if we should show a notification.
715      *
716      * <p>This is called when location access is granted to an app. In this case it is likely that
717      * the app will access the location soon. If this happens the notification will appear only a
718      * little after the user granted the location.
719      */
checkLocationAccessSoon()720     public void checkLocationAccessSoon() {
721         JobInfo.Builder b = (new JobInfo.Builder(LOCATION_ACCESS_CHECK_JOB_ID,
722                 new ComponentName(mContext, LocationAccessCheckJobService.class)))
723                 .setMinimumLatency(getDelayMillis());
724 
725         int scheduleResult = mJobScheduler.schedule(b.build());
726         if (scheduleResult != RESULT_SUCCESS) {
727             Log.e(LOG_TAG, "Could not schedule location access check " + scheduleResult);
728         }
729     }
730 
731     /**
732      * Check if the current user is the profile parent.
733      *
734      * @return {@code true} if the current user is the profile parent.
735      */
isRunningInParentProfile()736     private boolean isRunningInParentProfile() {
737         UserHandle user = UserHandle.of(myUserId());
738         UserHandle parent = mUserManager.getProfileParent(user);
739 
740         return parent == null || user.equals(parent);
741     }
742 
743     /**
744      * On boot set up a periodic job that starts checks.
745      */
746     public static class SetupPeriodicBackgroundLocationAccessCheck extends BroadcastReceiver {
747         @Override
onReceive(Context context, Intent intent)748         public void onReceive(Context context, Intent intent) {
749             LocationAccessCheck locationAccessCheck = new LocationAccessCheck(context, null);
750             JobScheduler jobScheduler = getSystemServiceSafe(context, JobScheduler.class);
751 
752             if (!locationAccessCheck.isRunningInParentProfile()) {
753                 // Profile parent handles child profiles too.
754                 return;
755             }
756 
757             // Init LocationAccessCheckEnabledTime if needed
758             locationAccessCheck.checkLocationAccessCheckEnabledAndUpdateEnabledTime();
759 
760             if (jobScheduler.getPendingJob(PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID) == null) {
761                 JobInfo.Builder b = (new JobInfo.Builder(PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID,
762                         new ComponentName(context, LocationAccessCheckJobService.class)))
763                         .setPeriodic(locationAccessCheck.getPeriodicCheckIntervalMillis(),
764                                 locationAccessCheck.getFlexForPeriodicCheckMillis());
765 
766                 int scheduleResult = jobScheduler.schedule(b.build());
767                 if (scheduleResult != RESULT_SUCCESS) {
768                     Log.e(LOG_TAG, "Could not schedule periodic location access check "
769                             + scheduleResult);
770                 }
771             }
772         }
773     }
774 
775     /**
776      * Checks if a new notification should be shown.
777      */
778     public static class LocationAccessCheckJobService extends JobService {
779         private LocationAccessCheck mLocationAccessCheck;
780 
781         /** If we currently check if we should show a notification, the task executing the check */
782         // @GuardedBy("sLock")
783         private @Nullable AddLocationNotificationIfNeededTask mAddLocationNotificationIfNeededTask;
784 
785         @Override
onCreate()786         public void onCreate() {
787             super.onCreate();
788             mLocationAccessCheck = new LocationAccessCheck(this, () -> {
789                 synchronized (sLock) {
790                     AddLocationNotificationIfNeededTask task = mAddLocationNotificationIfNeededTask;
791 
792                     return task != null && task.isCancelled();
793                 }
794             });
795         }
796 
797         /**
798          * Starts an asynchronous check if a location access notification should be shown.
799          *
800          * @param params Not used other than for interacting with job scheduling
801          *
802          * @return {@code false} iff another check if already running
803          */
804         @Override
onStartJob(JobParameters params)805         public boolean onStartJob(JobParameters params) {
806             synchronized (LocationAccessCheck.sLock) {
807                 if (mAddLocationNotificationIfNeededTask != null) {
808                     return false;
809                 }
810 
811                 mAddLocationNotificationIfNeededTask =
812                         new AddLocationNotificationIfNeededTask();
813 
814                 mAddLocationNotificationIfNeededTask.execute(params, this);
815             }
816 
817             return true;
818         }
819 
820         /**
821          * Abort the check if still running.
822          *
823          * @param params ignored
824          *
825          * @return false
826          */
827         @Override
onStopJob(JobParameters params)828         public boolean onStopJob(JobParameters params) {
829             AddLocationNotificationIfNeededTask task;
830             synchronized (sLock) {
831                 if (mAddLocationNotificationIfNeededTask == null) {
832                     return false;
833                 } else {
834                     task = mAddLocationNotificationIfNeededTask;
835                 }
836             }
837 
838             task.cancel(false);
839 
840             try {
841                 // Wait for task to finish
842                 task.get();
843             } catch (Exception e) {
844                 Log.e(LOG_TAG, "While waiting for " + task + " to finish", e);
845             }
846 
847             return false;
848         }
849 
850         /**
851          * A {@link AsyncTask task} that runs the check in the background.
852          */
853         private class AddLocationNotificationIfNeededTask extends
854                 AsyncTask<Object, Void, Void> {
855             @Override
doInBackground(Object... in)856             protected final Void doInBackground(Object... in) {
857                 JobParameters params = (JobParameters) in[0];
858                 LocationAccessCheckJobService service = (LocationAccessCheckJobService) in[1];
859                 mLocationAccessCheck.addLocationNotificationIfNeeded(params, service);
860                 return null;
861             }
862         }
863     }
864 
865     /**
866      * Handle the case where the notification is swiped away without further interaction.
867      */
868     public static class NotificationDeleteHandler extends BroadcastReceiver {
869         @Override
onReceive(Context context, Intent intent)870         public void onReceive(Context context, Intent intent) {
871             String pkg = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME);
872             UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER);
873             long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID);
874             int uid = intent.getIntExtra(EXTRA_UID, 0);
875 
876             PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId,
877                     uid, pkg,
878                     LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_DECLINED);
879             Log.v(LOG_TAG,
880                     "Location access check notification declined with sessionId=" + sessionId + ""
881                             + " uid=" + uid + " pkgName=" + pkg);
882 
883             new LocationAccessCheck(context, null).markAsNotified(pkg, user);
884         }
885     }
886 
887     /**
888      * Show the location permission switch when the notification is clicked.
889      */
890     public static class NotificationClickHandler extends BroadcastReceiver {
891         @Override
onReceive(Context context, Intent intent)892         public void onReceive(Context context, Intent intent) {
893             String pkg = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME);
894             UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER);
895             int uid = intent.getIntExtra(EXTRA_UID, 0);
896             long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID);
897 
898             new LocationAccessCheck(context, null).markAsNotified(pkg, user);
899 
900             PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId,
901                     uid, pkg,
902                     LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_CLICKED);
903             Log.v(LOG_TAG,
904                     "Location access check notification clicked with sessionId=" + sessionId + ""
905                             + " uid=" + uid + " pkgName=" + pkg);
906 
907             Intent manageAppPermission = new Intent(ACTION_MANAGE_APP_PERMISSION);
908             manageAppPermission.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK);
909             manageAppPermission.putExtra(EXTRA_PERMISSION_GROUP_NAME, LOCATION);
910             manageAppPermission.putExtra(EXTRA_PACKAGE_NAME, pkg);
911             manageAppPermission.putExtra(EXTRA_USER, user);
912             manageAppPermission.putExtra(EXTRA_SESSION_ID, sessionId);
913 
914 
915             context.startActivity(manageAppPermission);
916         }
917     }
918 
919     /**
920      * If a package gets removed or the data of the package gets cleared, forget that we showed a
921      * notification for it.
922      */
923     public static class PackageResetHandler extends BroadcastReceiver {
924         @Override
onReceive(Context context, Intent intent)925         public void onReceive(Context context, Intent intent) {
926             String action = intent.getAction();
927             if (!(Objects.equals(action, Intent.ACTION_PACKAGE_DATA_CLEARED)
928                     || Objects.equals(action, Intent.ACTION_PACKAGE_FULLY_REMOVED))) {
929                 return;
930             }
931 
932             Uri data = Preconditions.checkNotNull(intent.getData());
933             UserHandle user = getUserHandleForUid(intent.getIntExtra(EXTRA_UID, 0));
934 
935             if (DEBUG) Log.i(LOG_TAG, "Reset " + data.getSchemeSpecificPart());
936 
937             new LocationAccessCheck(context, null).forgetAboutPackage(
938                     data.getSchemeSpecificPart(), user);
939         }
940     }
941 
942     /**
943      * A immutable class containing a package name and a {@link UserHandle}.
944      */
945     private static final class UserPackage {
946         private final @NonNull Context mContext;
947 
948         public final @NonNull String pkg;
949         public final @NonNull UserHandle user;
950 
951         /**
952          * Create a new {@link UserPackage}
953          *
954          * @param context A context to be used by methods of this object
955          * @param pkg The name of the package
956          * @param user The user the package belongs to
957          */
UserPackage(@onNull Context context, @NonNull String pkg, @NonNull UserHandle user)958         UserPackage(@NonNull Context context, @NonNull String pkg, @NonNull UserHandle user) {
959             try {
960                 mContext = context.createPackageContextAsUser(context.getPackageName(), 0, user);
961             } catch (PackageManager.NameNotFoundException e) {
962                 throw new IllegalStateException(e);
963             }
964 
965             this.pkg = pkg;
966             this.user = user;
967         }
968 
969         /**
970          * Get {@link PackageInfo} for this user package.
971          *
972          * @return The package info
973          *
974          * @throws PackageManager.NameNotFoundException if package/user does not exist
975          */
getPackageInfo()976         @NonNull PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
977             return mContext.getPackageManager().getPackageInfo(pkg, GET_PERMISSIONS);
978         }
979 
980         /**
981          * Get the {@link AppPermissionGroup} for
982          * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and this user package.
983          *
984          * @return The app permission group or {@code null} if the app does not request location
985          */
getLocationGroup()986         @Nullable AppPermissionGroup getLocationGroup() {
987             try {
988                 return AppPermissionGroup.create(mContext, getPackageInfo(), ACCESS_FINE_LOCATION,
989                     false);
990             } catch (PackageManager.NameNotFoundException e) {
991                 return null;
992             }
993         }
994 
995         /**
996          * Get the {@link AppPermissionGroup} for the background location of
997          * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and this user package.
998          *
999          * @return The app permission group or {@code null} if the app does not request background
1000          *         location
1001          */
getBackgroundLocationGroup()1002         @Nullable AppPermissionGroup getBackgroundLocationGroup() {
1003             AppPermissionGroup locationGroup = getLocationGroup();
1004             if (locationGroup == null) {
1005                 return null;
1006             }
1007 
1008             return locationGroup.getBackgroundPermissions();
1009         }
1010 
1011         @Override
equals(Object o)1012         public boolean equals(Object o) {
1013             if (!(o instanceof UserPackage)) {
1014                 return false;
1015             }
1016 
1017             UserPackage userPackage = (UserPackage) o;
1018             return pkg.equals(userPackage.pkg) && user.equals(userPackage.user);
1019         }
1020 
1021         @Override
hashCode()1022         public int hashCode() {
1023             return Objects.hash(pkg, user);
1024         }
1025     }
1026 }
1027