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