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.packageinstaller.permission.service;
18 
19 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
20 import static android.app.AppOpsManager.OPSTR_FINE_LOCATION;
21 import static android.app.NotificationManager.IMPORTANCE_LOW;
22 import static android.app.PendingIntent.FLAG_ONE_SHOT;
23 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
24 import static android.app.PendingIntent.getBroadcast;
25 import static android.app.job.JobScheduler.RESULT_SUCCESS;
26 import static android.content.Context.MODE_PRIVATE;
27 import static android.content.Intent.EXTRA_PACKAGE_NAME;
28 import static android.content.Intent.EXTRA_PERMISSION_NAME;
29 import static android.content.Intent.EXTRA_UID;
30 import static android.content.Intent.EXTRA_USER;
31 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
32 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
33 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
34 import static android.content.pm.PackageManager.GET_PERMISSIONS;
35 import static android.graphics.Bitmap.Config.ARGB_8888;
36 import static android.graphics.Bitmap.createBitmap;
37 import static android.os.UserHandle.getUserHandleForUid;
38 import static android.os.UserHandle.myUserId;
39 import static android.provider.Settings.Secure.LOCATION_ACCESS_CHECK_DELAY_MILLIS;
40 import static android.provider.Settings.Secure.LOCATION_ACCESS_CHECK_INTERVAL_MILLIS;
41 
42 import static com.android.packageinstaller.Constants.EXTRA_SESSION_ID;
43 import static com.android.packageinstaller.Constants.INVALID_SESSION_ID;
44 import static com.android.packageinstaller.Constants.KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN;
45 import static com.android.packageinstaller.Constants.KEY_LOCATION_ACCESS_CHECK_ENABLED_TIME;
46 import static com.android.packageinstaller.Constants.LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE;
47 import static com.android.packageinstaller.Constants.LOCATION_ACCESS_CHECK_JOB_ID;
48 import static com.android.packageinstaller.Constants.LOCATION_ACCESS_CHECK_NOTIFICATION_ID;
49 import static com.android.packageinstaller.Constants.PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID;
50 import static com.android.packageinstaller.Constants.PERMISSION_REMINDER_CHANNEL_ID;
51 import static com.android.packageinstaller.Constants.PREFERENCES_FILE;
52 import static com.android.packageinstaller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION;
53 import static com.android.packageinstaller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_CLICKED;
54 import static com.android.packageinstaller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_DECLINED;
55 import static com.android.packageinstaller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_PRESENTED;
56 import static com.android.packageinstaller.permission.utils.Utils.OS_PKG;
57 import static com.android.packageinstaller.permission.utils.Utils.getParcelableExtraSafe;
58 import static com.android.packageinstaller.permission.utils.Utils.getParentUserContext;
59 import static com.android.packageinstaller.permission.utils.Utils.getStringExtraSafe;
60 import static com.android.packageinstaller.permission.utils.Utils.getSystemServiceSafe;
61 
62 import static java.lang.System.currentTimeMillis;
63 import static java.util.concurrent.TimeUnit.DAYS;
64 
65 import android.app.AppOpsManager;
66 import android.app.AppOpsManager.OpEntry;
67 import android.app.AppOpsManager.PackageOps;
68 import android.app.Notification;
69 import android.app.NotificationChannel;
70 import android.app.NotificationManager;
71 import android.app.job.JobInfo;
72 import android.app.job.JobParameters;
73 import android.app.job.JobScheduler;
74 import android.app.job.JobService;
75 import android.content.BroadcastReceiver;
76 import android.content.ComponentName;
77 import android.content.ContentResolver;
78 import android.content.Context;
79 import android.content.Intent;
80 import android.content.SharedPreferences;
81 import android.content.pm.PackageInfo;
82 import android.content.pm.PackageManager;
83 import android.content.pm.ResolveInfo;
84 import android.graphics.Bitmap;
85 import android.graphics.Canvas;
86 import android.graphics.drawable.Drawable;
87 import android.location.LocationManager;
88 import android.net.Uri;
89 import android.os.AsyncTask;
90 import android.os.Bundle;
91 import android.os.UserHandle;
92 import android.os.UserManager;
93 import android.provider.Settings;
94 import android.service.notification.StatusBarNotification;
95 import android.util.ArraySet;
96 import android.util.Log;
97 
98 import androidx.annotation.NonNull;
99 import androidx.annotation.Nullable;
100 import androidx.annotation.WorkerThread;
101 import androidx.core.util.Preconditions;
102 
103 import com.android.packageinstaller.PermissionControllerStatsLog;
104 import com.android.packageinstaller.permission.model.AppPermissionGroup;
105 import com.android.packageinstaller.permission.ui.AppPermissionActivity;
106 import com.android.packageinstaller.permission.utils.Utils;
107 import com.android.permissioncontroller.R;
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 = getNotificationAppName();
591 
592         Notification.Builder b = (new Notification.Builder(mContext,
593                 PERMISSION_REMINDER_CHANNEL_ID))
594                 .setContentTitle(mContext.getString(
595                         R.string.background_location_access_reminder_notification_title, pkgLabel))
596                 .setContentText(mContext.getString(
597                         R.string.background_location_access_reminder_notification_content))
598                 .setStyle(new Notification.BigTextStyle().bigText(mContext.getString(
599                         R.string.background_location_access_reminder_notification_content)))
600                 .setSmallIcon(R.drawable.ic_pin_drop)
601                 .setLargeIcon(pkgIconBmp)
602                 .setColor(mContext.getColor(android.R.color.system_notification_accent_color))
603                 .setAutoCancel(true)
604                 .setDeleteIntent(getBroadcast(mContext, 0, deleteIntent,
605                         FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT))
606                 .setContentIntent(getBroadcast(mContext, 0, clickIntent,
607                         FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT));
608 
609         if (appName != null) {
610             Bundle extras = new Bundle();
611             extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, appName.toString());
612             b.addExtras(extras);
613         }
614 
615         notificationManager.notify(pkgName, LOCATION_ACCESS_CHECK_NOTIFICATION_ID, b.build());
616 
617         if (DEBUG) Log.i(LOG_TAG, "Notified " + pkgName);
618 
619         PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId,
620                 pkg.applicationInfo.uid, pkgName,
621                 LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_PRESENTED);
622         Log.v(LOG_TAG, "Location access check notification shown with sessionId=" + sessionId + ""
623                 + " uid=" + pkg.applicationInfo.uid + " pkgName=" + pkgName);
624 
625         mSharedPrefs.edit().putLong(KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN,
626                 currentTimeMillis()).apply();
627     }
628 
629     @Nullable
getNotificationAppName()630     private CharSequence getNotificationAppName() {
631         // We pretend we're the Settings app sending the notification, so figure out its name.
632         Intent openSettingsIntent = new Intent(Settings.ACTION_SETTINGS);
633         ResolveInfo resolveInfo = mPackageManager.resolveActivity(openSettingsIntent, 0);
634         if (resolveInfo == null) {
635             return null;
636         }
637         return mPackageManager.getApplicationLabel(resolveInfo.activityInfo.applicationInfo);
638     }
639 
640     /**
641      * Get currently shown notification. We only ever show one notification per profile group.
642      *
643      * @return The notification or {@code null} if no notification is currently shown
644      */
getCurrentlyShownNotificationLocked()645     private @Nullable StatusBarNotification getCurrentlyShownNotificationLocked() {
646         List<UserHandle> profiles = mUserManager.getUserProfiles();
647 
648         int numProfiles = profiles.size();
649         for (int profileNum = 0; profileNum < numProfiles; profileNum++) {
650             NotificationManager notificationManager = getSystemServiceSafe(mContext,
651                     NotificationManager.class, profiles.get(profileNum));
652 
653             StatusBarNotification[] notifications = notificationManager.getActiveNotifications();
654 
655             int numNotifications = notifications.length;
656             for (int notificationNum = 0; notificationNum < numNotifications; notificationNum++) {
657                 StatusBarNotification notification = notifications[notificationNum];
658 
659                 if (notification.getId() == LOCATION_ACCESS_CHECK_NOTIFICATION_ID) {
660                     return notification;
661                 }
662             }
663         }
664 
665         return null;
666     }
667 
668     /**
669      * Go through the list of packages we already shown a notification for and remove those that do
670      * not request fine background location access.
671      *
672      * @param alreadyNotifiedPkgs The packages we already shown a notification for. This paramter is
673      *                            modified inside of this method.
674      *
675      * @throws InterruptedException If {@link #mShouldCancel}
676      */
resetAlreadyNotifiedPackagesWithoutPermissionLocked( @onNull ArraySet<UserPackage> alreadyNotifiedPkgs)677     private void resetAlreadyNotifiedPackagesWithoutPermissionLocked(
678             @NonNull ArraySet<UserPackage> alreadyNotifiedPkgs) throws InterruptedException {
679         ArrayList<UserPackage> packagesToRemove = new ArrayList<>();
680 
681         for (UserPackage userPkg : alreadyNotifiedPkgs) {
682             throwInterruptedExceptionIfTaskIsCanceled();
683 
684             AppPermissionGroup bgLocationGroup = userPkg.getBackgroundLocationGroup();
685             if (bgLocationGroup == null || !bgLocationGroup.areRuntimePermissionsGranted()) {
686                 packagesToRemove.add(userPkg);
687             }
688         }
689 
690         if (!packagesToRemove.isEmpty()) {
691             alreadyNotifiedPkgs.removeAll(packagesToRemove);
692             safeAlreadyNotifiedPackagesLocked(alreadyNotifiedPkgs);
693             throwInterruptedExceptionIfTaskIsCanceled();
694         }
695     }
696 
697     /**
698      * Remove all persisted state for a package.
699      *
700      * @param pkg name of package
701      * @param user user the package belongs to
702      */
forgetAboutPackage(@onNull String pkg, @NonNull UserHandle user)703     private void forgetAboutPackage(@NonNull String pkg, @NonNull UserHandle user) {
704         synchronized (sLock) {
705             StatusBarNotification notification = getCurrentlyShownNotificationLocked();
706             if (notification != null && notification.getUser().equals(user)
707                     && notification.getTag().equals(pkg)) {
708                 getSystemServiceSafe(mContext, NotificationManager.class, user).cancel(
709                         pkg, LOCATION_ACCESS_CHECK_NOTIFICATION_ID);
710             }
711 
712             ArraySet<UserPackage> packages = loadAlreadyNotifiedPackagesLocked();
713             packages.remove(new UserPackage(mContext, pkg, user));
714             safeAlreadyNotifiedPackagesLocked(packages);
715         }
716     }
717 
718     /**
719      * After a small delay schedule a check if we should show a notification.
720      *
721      * <p>This is called when location access is granted to an app. In this case it is likely that
722      * the app will access the location soon. If this happens the notification will appear only a
723      * little after the user granted the location.
724      */
checkLocationAccessSoon()725     public void checkLocationAccessSoon() {
726         JobInfo.Builder b = (new JobInfo.Builder(LOCATION_ACCESS_CHECK_JOB_ID,
727                 new ComponentName(mContext, LocationAccessCheckJobService.class)))
728                 .setMinimumLatency(getDelayMillis());
729 
730         int scheduleResult = mJobScheduler.schedule(b.build());
731         if (scheduleResult != RESULT_SUCCESS) {
732             Log.e(LOG_TAG, "Could not schedule location access check " + scheduleResult);
733         }
734     }
735 
736     /**
737      * Check if the current user is the profile parent.
738      *
739      * @return {@code true} if the current user is the profile parent.
740      */
isRunningInParentProfile()741     private boolean isRunningInParentProfile() {
742         UserHandle user = UserHandle.of(myUserId());
743         UserHandle parent = mUserManager.getProfileParent(user);
744 
745         return parent == null || user.equals(parent);
746     }
747 
748     /**
749      * On boot set up a periodic job that starts checks.
750      */
751     public static class SetupPeriodicBackgroundLocationAccessCheck extends BroadcastReceiver {
752         @Override
onReceive(Context context, Intent intent)753         public void onReceive(Context context, Intent intent) {
754             LocationAccessCheck locationAccessCheck = new LocationAccessCheck(context, null);
755             JobScheduler jobScheduler = getSystemServiceSafe(context, JobScheduler.class);
756 
757             if (!locationAccessCheck.isRunningInParentProfile()) {
758                 // Profile parent handles child profiles too.
759                 return;
760             }
761 
762             if (jobScheduler.getPendingJob(PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID) == null) {
763                 JobInfo.Builder b = (new JobInfo.Builder(PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID,
764                         new ComponentName(context, LocationAccessCheckJobService.class)))
765                         .setPeriodic(locationAccessCheck.getPeriodicCheckIntervalMillis(),
766                                 locationAccessCheck.getFlexForPeriodicCheckMillis());
767 
768                 int scheduleResult = jobScheduler.schedule(b.build());
769                 if (scheduleResult != RESULT_SUCCESS) {
770                     Log.e(LOG_TAG, "Could not schedule periodic location access check "
771                             + scheduleResult);
772                 }
773             }
774         }
775     }
776 
777     /**
778      * Checks if a new notification should be shown.
779      */
780     public static class LocationAccessCheckJobService extends JobService {
781         private LocationAccessCheck mLocationAccessCheck;
782 
783         /** If we currently check if we should show a notification, the task executing the check */
784         // @GuardedBy("sLock")
785         private @Nullable AddLocationNotificationIfNeededTask mAddLocationNotificationIfNeededTask;
786 
787         @Override
onCreate()788         public void onCreate() {
789             super.onCreate();
790             mLocationAccessCheck = new LocationAccessCheck(this, () -> {
791                 synchronized (sLock) {
792                     AddLocationNotificationIfNeededTask task = mAddLocationNotificationIfNeededTask;
793 
794                     return task != null && task.isCancelled();
795                 }
796             });
797         }
798 
799         /**
800          * Starts an asynchronous check if a location access notification should be shown.
801          *
802          * @param params Not used other than for interacting with job scheduling
803          *
804          * @return {@code false} iff another check if already running
805          */
806         @Override
onStartJob(JobParameters params)807         public boolean onStartJob(JobParameters params) {
808             synchronized (LocationAccessCheck.sLock) {
809                 if (mAddLocationNotificationIfNeededTask != null) {
810                     return false;
811                 }
812 
813                 mAddLocationNotificationIfNeededTask =
814                         new AddLocationNotificationIfNeededTask();
815 
816                 mAddLocationNotificationIfNeededTask.execute(params, this);
817             }
818 
819             return true;
820         }
821 
822         /**
823          * Abort the check if still running.
824          *
825          * @param params ignored
826          *
827          * @return false
828          */
829         @Override
onStopJob(JobParameters params)830         public boolean onStopJob(JobParameters params) {
831             AddLocationNotificationIfNeededTask task;
832             synchronized (sLock) {
833                 if (mAddLocationNotificationIfNeededTask == null) {
834                     return false;
835                 } else {
836                     task = mAddLocationNotificationIfNeededTask;
837                 }
838             }
839 
840             task.cancel(false);
841 
842             try {
843                 // Wait for task to finish
844                 task.get();
845             } catch (Exception e) {
846                 Log.e(LOG_TAG, "While waiting for " + task + " to finish", e);
847             }
848 
849             return false;
850         }
851 
852         /**
853          * A {@link AsyncTask task} that runs the check in the background.
854          */
855         private class AddLocationNotificationIfNeededTask extends
856                 AsyncTask<Object, Void, Void> {
857             @Override
doInBackground(Object... in)858             protected final Void doInBackground(Object... in) {
859                 JobParameters params = (JobParameters) in[0];
860                 LocationAccessCheckJobService service = (LocationAccessCheckJobService) in[1];
861                 mLocationAccessCheck.addLocationNotificationIfNeeded(params, service);
862                 return null;
863             }
864         }
865     }
866 
867     /**
868      * Handle the case where the notification is swiped away without further interaction.
869      */
870     public static class NotificationDeleteHandler extends BroadcastReceiver {
871         @Override
onReceive(Context context, Intent intent)872         public void onReceive(Context context, Intent intent) {
873             String pkg = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME);
874             UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER);
875             long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID);
876             int uid = intent.getIntExtra(EXTRA_UID, 0);
877 
878             PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId,
879                     uid, pkg,
880                     LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_DECLINED);
881             Log.v(LOG_TAG,
882                     "Location access check notification declined with sessionId=" + sessionId + ""
883                             + " uid=" + uid + " pkgName=" + pkg);
884 
885             new LocationAccessCheck(context, null).markAsNotified(pkg, user);
886         }
887     }
888 
889     /**
890      * Show the location permission switch when the notification is clicked.
891      */
892     public static class NotificationClickHandler extends BroadcastReceiver {
893         @Override
onReceive(Context context, Intent intent)894         public void onReceive(Context context, Intent intent) {
895             String pkg = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME);
896             UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER);
897             int uid = intent.getIntExtra(EXTRA_UID, 0);
898             long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID);
899 
900             new LocationAccessCheck(context, null).markAsNotified(pkg, user);
901 
902             PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId,
903                     uid, pkg,
904                     LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_CLICKED);
905             Log.v(LOG_TAG,
906                     "Location access check notification clicked with sessionId=" + sessionId + ""
907                             + " uid=" + uid + " pkgName=" + pkg);
908 
909             Intent manageAppPermission = new Intent(context, AppPermissionActivity.class);
910             manageAppPermission.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK);
911             manageAppPermission.putExtra(EXTRA_PERMISSION_NAME, ACCESS_FINE_LOCATION);
912             manageAppPermission.putExtra(EXTRA_PACKAGE_NAME, pkg);
913             manageAppPermission.putExtra(EXTRA_USER, user);
914             manageAppPermission.putExtra(EXTRA_SESSION_ID, sessionId);
915 
916 
917             context.startActivity(manageAppPermission);
918         }
919     }
920 
921     /**
922      * If a package gets removed or the data of the package gets cleared, forget that we showed a
923      * notification for it.
924      */
925     public static class PackageResetHandler extends BroadcastReceiver {
926         @Override
onReceive(Context context, Intent intent)927         public void onReceive(Context context, Intent intent) {
928             String action = intent.getAction();
929             if (!(Objects.equals(action, Intent.ACTION_PACKAGE_DATA_CLEARED)
930                     || Objects.equals(action, Intent.ACTION_PACKAGE_FULLY_REMOVED))) {
931                 return;
932             }
933 
934             Uri data = Preconditions.checkNotNull(intent.getData());
935             UserHandle user = getUserHandleForUid(intent.getIntExtra(EXTRA_UID, 0));
936 
937             if (DEBUG) Log.i(LOG_TAG, "Reset " + data.getSchemeSpecificPart());
938 
939             new LocationAccessCheck(context, null).forgetAboutPackage(
940                     data.getSchemeSpecificPart(), user);
941         }
942     }
943 
944     /**
945      * A immutable class containing a package name and a {@link UserHandle}.
946      */
947     private static final class UserPackage {
948         private final @NonNull Context mContext;
949 
950         public final @NonNull String pkg;
951         public final @NonNull UserHandle user;
952 
953         /**
954          * Create a new {@link UserPackage}
955          *
956          * @param context A context to be used by methods of this object
957          * @param pkg The name of the package
958          * @param user The user the package belongs to
959          */
UserPackage(@onNull Context context, @NonNull String pkg, @NonNull UserHandle user)960         UserPackage(@NonNull Context context, @NonNull String pkg, @NonNull UserHandle user) {
961             try {
962                 mContext = context.createPackageContextAsUser(context.getPackageName(), 0, user);
963             } catch (PackageManager.NameNotFoundException e) {
964                 throw new IllegalStateException(e);
965             }
966 
967             this.pkg = pkg;
968             this.user = user;
969         }
970 
971         /**
972          * Get {@link PackageInfo} for this user package.
973          *
974          * @return The package info
975          *
976          * @throws PackageManager.NameNotFoundException if package/user does not exist
977          */
getPackageInfo()978         @NonNull PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
979             return mContext.getPackageManager().getPackageInfo(pkg, GET_PERMISSIONS);
980         }
981 
982         /**
983          * Get the {@link AppPermissionGroup} for
984          * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and this user package.
985          *
986          * @return The app permission group or {@code null} if the app does not request location
987          */
getLocationGroup()988         @Nullable AppPermissionGroup getLocationGroup() {
989             try {
990                 return AppPermissionGroup.create(mContext, getPackageInfo(), ACCESS_FINE_LOCATION,
991                     false);
992             } catch (PackageManager.NameNotFoundException e) {
993                 return null;
994             }
995         }
996 
997         /**
998          * Get the {@link AppPermissionGroup} for the background location of
999          * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and this user package.
1000          *
1001          * @return The app permission group or {@code null} if the app does not request background
1002          *         location
1003          */
getBackgroundLocationGroup()1004         @Nullable AppPermissionGroup getBackgroundLocationGroup() {
1005             AppPermissionGroup locationGroup = getLocationGroup();
1006             if (locationGroup == null) {
1007                 return null;
1008             }
1009 
1010             return locationGroup.getBackgroundPermissions();
1011         }
1012 
1013         @Override
equals(Object o)1014         public boolean equals(Object o) {
1015             if (!(o instanceof UserPackage)) {
1016                 return false;
1017             }
1018 
1019             UserPackage userPackage = (UserPackage) o;
1020             return pkg.equals(userPackage.pkg) && user.equals(userPackage.user);
1021         }
1022 
1023         @Override
hashCode()1024         public int hashCode() {
1025             return Objects.hash(pkg, user);
1026         }
1027     }
1028 }
1029