• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.permissioncontroller.permission.service;
18 
19 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
20 import static android.Manifest.permission_group.LOCATION;
21 import static android.app.AppOpsManager.OPSTR_FINE_LOCATION;
22 import static android.app.NotificationManager.IMPORTANCE_LOW;
23 import static android.app.PendingIntent.FLAG_IMMUTABLE;
24 import static android.app.PendingIntent.FLAG_ONE_SHOT;
25 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
26 import static android.app.job.JobScheduler.RESULT_SUCCESS;
27 import static android.content.Context.MODE_PRIVATE;
28 import static android.content.Intent.ACTION_MANAGE_APP_PERMISSION;
29 import static android.content.Intent.ACTION_SAFETY_CENTER;
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_DECLINED;
57 import static com.android.permissioncontroller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_PRESENTED;
58 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION;
59 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CARD_DISMISSED;
60 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CLICKED_CTA1;
61 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__BG_LOCATION;
62 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION;
63 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__DISMISSED;
64 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_SHOWN;
65 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__BG_LOCATION;
66 import static com.android.permissioncontroller.permission.utils.Utils.OS_PKG;
67 import static com.android.permissioncontroller.permission.utils.Utils.getParcelableExtraSafe;
68 import static com.android.permissioncontroller.permission.utils.Utils.getParentUserContext;
69 import static com.android.permissioncontroller.permission.utils.Utils.getStringExtraSafe;
70 import static com.android.permissioncontroller.permission.utils.Utils.getSystemServiceSafe;
71 
72 import static java.lang.System.currentTimeMillis;
73 import static java.util.concurrent.TimeUnit.DAYS;
74 
75 import android.app.AppOpsManager;
76 import android.app.AppOpsManager.OpEntry;
77 import android.app.AppOpsManager.PackageOps;
78 import android.app.Notification;
79 import android.app.NotificationChannel;
80 import android.app.NotificationManager;
81 import android.app.PendingIntent;
82 import android.app.job.JobInfo;
83 import android.app.job.JobParameters;
84 import android.app.job.JobScheduler;
85 import android.app.job.JobService;
86 import android.content.BroadcastReceiver;
87 import android.content.ComponentName;
88 import android.content.ContentResolver;
89 import android.content.Context;
90 import android.content.Intent;
91 import android.content.SharedPreferences;
92 import android.content.pm.PackageInfo;
93 import android.content.pm.PackageManager;
94 import android.graphics.Bitmap;
95 import android.graphics.Canvas;
96 import android.graphics.drawable.Drawable;
97 import android.graphics.drawable.Icon;
98 import android.location.LocationManager;
99 import android.net.Uri;
100 import android.os.AsyncTask;
101 import android.os.Build;
102 import android.os.Bundle;
103 import android.os.UserHandle;
104 import android.os.UserManager;
105 import android.provider.DeviceConfig;
106 import android.provider.Settings;
107 import android.safetycenter.SafetyCenterManager;
108 import android.safetycenter.SafetyEvent;
109 import android.safetycenter.SafetySourceData;
110 import android.safetycenter.SafetySourceIssue;
111 import android.safetycenter.SafetySourceIssue.Action;
112 import android.service.notification.StatusBarNotification;
113 import android.text.TextUtils;
114 import android.util.ArrayMap;
115 import android.util.ArraySet;
116 import android.util.Log;
117 
118 import androidx.annotation.ChecksSdkIntAtLeast;
119 import androidx.annotation.NonNull;
120 import androidx.annotation.Nullable;
121 import androidx.annotation.RequiresApi;
122 import androidx.annotation.WorkerThread;
123 import androidx.core.util.Preconditions;
124 
125 import com.android.modules.utils.build.SdkLevel;
126 import com.android.permissioncontroller.PermissionControllerStatsLog;
127 import com.android.permissioncontroller.R;
128 import com.android.permissioncontroller.permission.model.AppPermissionGroup;
129 import com.android.permissioncontroller.permission.utils.KotlinUtils;
130 import com.android.permissioncontroller.permission.utils.Utils;
131 
132 import java.io.BufferedReader;
133 import java.io.BufferedWriter;
134 import java.io.FileNotFoundException;
135 import java.io.IOException;
136 import java.io.InputStreamReader;
137 import java.io.OutputStreamWriter;
138 import java.util.ArrayList;
139 import java.util.List;
140 import java.util.Map;
141 import java.util.Objects;
142 import java.util.Random;
143 import java.util.Set;
144 import java.util.function.BooleanSupplier;
145 import java.util.stream.Collectors;
146 
147 /**
148  * Show notification that double-guesses the user if she/he really wants to grant fine background
149  * location access to an app.
150  *
151  * <p>A notification is scheduled after the background permission access is granted via
152  * {@link #checkLocationAccessSoon()} or periodically.
153  *
154  * <p>We rate limit the number of notification we show and only ever show one notification at a
155  * time. Further we only shown notifications if the app has actually accessed the fine location
156  * in the background.
157  *
158  * <p>As there are many cases why a notification should not been shown, we always schedule a
159  * {@link #addLocationNotificationIfNeeded check} which then might add a notification.
160  */
161 public class LocationAccessCheck {
162     private static final String LOG_TAG = LocationAccessCheck.class.getSimpleName();
163     private static final boolean DEBUG = false;
164     private static final long DEFAULT_RENOTIFY_DURATION_MILLIS = DAYS.toMillis(90);
165     private static final String ISSUE_ID_PREFIX = "bg_location_";
166     private static final String ISSUE_TYPE_ID = "bg_location_privacy_issue";
167     private static final String REVOKE_LOCATION_ACCESS_ID_PREFIX = "revoke_location_access_";
168     private static final String VIEW_LOCATION_ACCESS_ID = "view_location_access";
169     public static final String BG_LOCATION_SOURCE_ID = "AndroidBackgroundLocation";
170 
171     /**
172      * Device config property for delay in milliseconds
173      * between granting a permission and the follow up check
174      **/
175     public static final String PROPERTY_LOCATION_ACCESS_CHECK_DELAY_MILLIS =
176             "location_access_check_delay_millis";
177 
178     /**
179      * Device config property for delay in milliseconds
180      * between periodic checks for background location access
181      **/
182     public static final String PROPERTY_LOCATION_ACCESS_PERIODIC_INTERVAL_MILLIS =
183             "location_access_check_periodic_interval_millis";
184 
185     /**
186      * Device config property for flag that determines whether location check for safety center
187      * is enabled.
188      */
189     public static final String PROPERTY_BG_LOCATION_CHECK_ENABLED = "bg_location_check_is_enabled";
190 
191     /**
192      * Lock required for all methods called {@code ...Locked}
193      */
194     private static final Object sLock = new Object();
195 
196     private final Random mRandom = new Random();
197 
198     private final @NonNull Context mContext;
199     private final @NonNull JobScheduler mJobScheduler;
200     private final @NonNull ContentResolver mContentResolver;
201     private final @NonNull AppOpsManager mAppOpsManager;
202     private final @NonNull PackageManager mPackageManager;
203     private final @NonNull UserManager mUserManager;
204     private final @NonNull SharedPreferences mSharedPrefs;
205 
206     /**
207      * If the current long running operation should be canceled
208      */
209     private final @Nullable BooleanSupplier mShouldCancel;
210 
211     /**
212      * Get time in between two periodic checks.
213      *
214      * <p>Default: 1 day
215      *
216      * @return The time in between check in milliseconds
217      */
getPeriodicCheckIntervalMillis()218     private long getPeriodicCheckIntervalMillis() {
219         return SdkLevel.isAtLeastT() ? DeviceConfig.getLong(DeviceConfig.NAMESPACE_PRIVACY,
220                 PROPERTY_LOCATION_ACCESS_PERIODIC_INTERVAL_MILLIS, DAYS.toMillis(1))
221                 : Settings.Secure.getLong(mContentResolver,
222                         LOCATION_ACCESS_CHECK_INTERVAL_MILLIS, DAYS.toMillis(1));
223     }
224 
225     /**
226      * Flexibility of the periodic check.
227      *
228      * <p>10% of {@link #getPeriodicCheckIntervalMillis()}
229      *
230      * @return The flexibility of the periodic check in milliseconds
231      */
getFlexForPeriodicCheckMillis()232     private long getFlexForPeriodicCheckMillis() {
233         return getPeriodicCheckIntervalMillis() / 10;
234     }
235 
236     /**
237      * Get the delay in between granting a permission and the follow up check.
238      *
239      * <p>Default: 1 day
240      *
241      * @return The delay in milliseconds
242      */
getDelayMillis()243     private long getDelayMillis() {
244         return SdkLevel.isAtLeastT() ? DeviceConfig.getLong(DeviceConfig.NAMESPACE_PRIVACY,
245                 PROPERTY_LOCATION_ACCESS_CHECK_DELAY_MILLIS, DAYS.toMillis(1))
246                 : Settings.Secure.getLong(mContentResolver, LOCATION_ACCESS_CHECK_DELAY_MILLIS,
247                         DAYS.toMillis(1));
248     }
249 
250     /**
251      * Minimum time in between showing two notifications.
252      *
253      * <p>This is just small enough so that the periodic check can always show a notification.
254      *
255      * @return The minimum time in milliseconds
256      */
getInBetweenNotificationsMillis()257     private long getInBetweenNotificationsMillis() {
258         return getPeriodicCheckIntervalMillis() - (long) (getFlexForPeriodicCheckMillis() * 2.1);
259     }
260 
261     /**
262      * Load the list of {@link UserPackage packages} we already shown a notification for.
263      *
264      * @return The list of packages we already shown a notification for.
265      */
loadAlreadyNotifiedPackagesLocked()266     private @NonNull ArraySet<UserPackage> loadAlreadyNotifiedPackagesLocked() {
267         try (BufferedReader reader = new BufferedReader(new InputStreamReader(
268             mContext.openFileInput(LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE)))) {
269             ArraySet<UserPackage> packages = new ArraySet<>();
270 
271             /*
272              * The format of the file is <package> <serial of user> <dismissed in safety center>,
273              * Since notification timestamp was added later it is possible that it might be
274              * missing during the first check. We need to handle that.
275              *
276              * e.g.
277              * com.one.package 5630633845 true
278              * com.two.package 5630633853 false
279              * com.three.package 5630633853 false
280              */
281             while (true) {
282                 String line = reader.readLine();
283                 if (line == null) {
284                     break;
285                 }
286                 String[] lineComponents = line.split(" ");
287                 String pkg = lineComponents[0];
288                 UserHandle user = mUserManager.getUserForSerialNumber(
289                         Long.valueOf(lineComponents[1]));
290                 boolean dismissedInSafetyCenter = lineComponents.length == 3
291                         ? Boolean.valueOf(lineComponents[2]) : false;
292                 if (user != null) {
293                     UserPackage userPkg = new UserPackage(mContext, pkg, user,
294                             dismissedInSafetyCenter);
295                     packages.add(userPkg);
296                 } else {
297                     Log.i(LOG_TAG, "Not restoring state \"" + line + "\" as user is unknown");
298                 }
299             }
300             return packages;
301         } catch (FileNotFoundException ignored) {
302             return new ArraySet<>();
303         } catch (Exception e) {
304             Log.w(LOG_TAG, "Could not read " + LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE, e);
305             return new ArraySet<>();
306         }
307     }
308 
309     /**
310      * Persist the list of {@link UserPackage packages} we have already shown a notification for.
311      *
312      * @param packages The list of packages we already shown a notification for.
313      */
persistAlreadyNotifiedPackagesLocked(@onNull ArraySet<UserPackage> packages)314     private void persistAlreadyNotifiedPackagesLocked(@NonNull ArraySet<UserPackage> packages) {
315         try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
316                 mContext.openFileOutput(LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE,
317                         MODE_PRIVATE)))) {
318             /*
319              * The format of the file is <package> <serial of user> <dismissed in safety center>,
320              * e.g.
321              * com.one.package 5630633845 true
322              * com.two.package 5630633853 false
323              * com.three.package 5630633853 false
324              */
325             int numPkgs = packages.size();
326             for (int i = 0; i < numPkgs; i++) {
327                 UserPackage userPkg = packages.valueAt(i);
328                 writer.append(userPkg.pkg);
329                 writer.append(' ');
330                 writer.append(
331                         Long.valueOf(mUserManager.getSerialNumberForUser(userPkg.user)).toString());
332                 writer.append(' ');
333                 writer.append(Boolean.toString(userPkg.dismissedInSafetyCenter));
334                 writer.newLine();
335             }
336         } catch (IOException e) {
337             Log.e(LOG_TAG, "Could not write " + LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE, e);
338         }
339     }
340 
341     /**
342      * Remember that we showed a notification for a {@link UserPackage}
343      *
344      * @param pkg                     The package we notified for
345      * @param user                    The user we notified for
346      * @param dismissedInSafetyCenter Whether this warning was dismissed by the user in safety
347      *                                center
348      */
markAsNotified(@onNull String pkg, @NonNull UserHandle user, boolean dismissedInSafetyCenter)349     private void markAsNotified(@NonNull String pkg, @NonNull UserHandle user,
350             boolean dismissedInSafetyCenter) {
351         synchronized (sLock) {
352             ArraySet<UserPackage> alreadyNotifiedPackages = loadAlreadyNotifiedPackagesLocked();
353             UserPackage userPackage = new UserPackage(mContext, pkg, user, dismissedInSafetyCenter);
354             // Remove stale persisted info
355             alreadyNotifiedPackages.remove(userPackage);
356             // Persist new info about the package
357             alreadyNotifiedPackages.add(userPackage);
358             persistAlreadyNotifiedPackagesLocked(alreadyNotifiedPackages);
359         }
360     }
361 
362     /**
363      * Create the channel the location access notifications should be posted to.
364      *
365      * @param user The user to create the channel for
366      */
createPermissionReminderChannel(@onNull UserHandle user)367     private void createPermissionReminderChannel(@NonNull UserHandle user) {
368         NotificationManager notificationManager = getSystemServiceSafe(mContext,
369                 NotificationManager.class, user);
370 
371         NotificationChannel permissionReminderChannel = new NotificationChannel(
372                 PERMISSION_REMINDER_CHANNEL_ID, mContext.getString(R.string.permission_reminders),
373                 IMPORTANCE_LOW);
374         notificationManager.createNotificationChannel(permissionReminderChannel);
375     }
376 
377     /**
378      * If {@link #mShouldCancel} throw an {@link InterruptedException}.
379      */
throwInterruptedExceptionIfTaskIsCanceled()380     private void throwInterruptedExceptionIfTaskIsCanceled() throws InterruptedException {
381         if (mShouldCancel != null && mShouldCancel.getAsBoolean()) {
382             throw new InterruptedException();
383         }
384     }
385 
386     /**
387      * Create a new {@link LocationAccessCheck} object.
388      *
389      * @param context      Used to resolve managers
390      * @param shouldCancel If supplied, can be used to interrupt long running operations
391      */
LocationAccessCheck(@onNull Context context, @Nullable BooleanSupplier shouldCancel)392     public LocationAccessCheck(@NonNull Context context, @Nullable BooleanSupplier shouldCancel) {
393         mContext = getParentUserContext(context);
394         mJobScheduler = getSystemServiceSafe(mContext, JobScheduler.class);
395         mAppOpsManager = getSystemServiceSafe(mContext, AppOpsManager.class);
396         mPackageManager = mContext.getPackageManager();
397         mUserManager = getSystemServiceSafe(mContext, UserManager.class);
398         mSharedPrefs = mContext.getSharedPreferences(PREFERENCES_FILE, MODE_PRIVATE);
399         mContentResolver = mContext.getContentResolver();
400         mShouldCancel = shouldCancel;
401     }
402 
403     /**
404      * Check if a location access notification should be shown and then add it.
405      *
406      * <p>Always run async inside a
407      * {@link LocationAccessCheckJobService.AddLocationNotificationIfNeededTask}.
408      */
409     @WorkerThread
addLocationNotificationIfNeeded(@onNull JobParameters params, @NonNull LocationAccessCheckJobService service)410     private void addLocationNotificationIfNeeded(@NonNull JobParameters params,
411             @NonNull LocationAccessCheckJobService service) {
412         if (!checkLocationAccessCheckEnabledAndUpdateEnabledTime()) {
413             Log.v(LOG_TAG, "LocationAccessCheck feature is not enabled.");
414             service.jobFinished(params, false);
415             return;
416         }
417 
418         synchronized (sLock) {
419             try {
420                 if (currentTimeMillis() - mSharedPrefs.getLong(
421                         KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN, 0)
422                         < getInBetweenNotificationsMillis()) {
423                     Log.v(LOG_TAG, "location notification interval is not enough.");
424                     service.jobFinished(params, false);
425                     return;
426                 }
427 
428                 if (getCurrentlyShownNotificationLocked() != null) {
429                     Log.v(LOG_TAG, "already location notification exist.");
430                     service.jobFinished(params, false);
431                     return;
432                 }
433 
434                 addLocationNotificationIfNeeded(mAppOpsManager.getPackagesForOps(
435                         new String[]{OPSTR_FINE_LOCATION}));
436                 service.jobFinished(params, false);
437             } catch (Exception e) {
438                 Log.e(LOG_TAG, "Could not check for location access", e);
439                 service.jobFinished(params, true);
440             } finally {
441                 synchronized (sLock) {
442                     service.mAddLocationNotificationIfNeededTask = null;
443                     Log.v(LOG_TAG, "LocationAccessCheck privacy job marked complete.");
444                 }
445             }
446         }
447     }
448 
addLocationNotificationIfNeeded(@onNull List<PackageOps> ops)449     private void addLocationNotificationIfNeeded(@NonNull List<PackageOps> ops)
450             throws InterruptedException {
451         synchronized (sLock) {
452             List<UserPackage> packages = getLocationUsersLocked(ops);
453             ArraySet<UserPackage> alreadyNotifiedPackages = loadAlreadyNotifiedPackagesLocked();
454             if (DEBUG) {
455                 Log.v(LOG_TAG, "location packages: " + packages);
456                 Log.v(LOG_TAG, "already notified packages: " + alreadyNotifiedPackages);
457             }
458             throwInterruptedExceptionIfTaskIsCanceled();
459             // Send these issues to safety center
460             if (isSafetyCenterBgLocationReminderEnabled()) {
461                 SafetyEvent safetyEvent = new SafetyEvent.Builder(
462                         SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED).build();
463                 sendToSafetyCenter(packages, safetyEvent, alreadyNotifiedPackages, null);
464             }
465             filterAlreadyNotifiedPackagesLocked(packages, alreadyNotifiedPackages);
466 
467             // Get a random package and resolve package info
468             PackageInfo pkgInfo = null;
469             while (pkgInfo == null) {
470                 throwInterruptedExceptionIfTaskIsCanceled();
471 
472                 if (packages.isEmpty()) {
473                     if (DEBUG) {
474                         Log.v(LOG_TAG, "No package found to send a notification");
475                     }
476                     return;
477                 }
478 
479                 UserPackage packageToNotifyFor = null;
480 
481                 // Prefer to show notification for location controller extra package
482                 int numPkgs = packages.size();
483                 for (int i = 0; i < numPkgs; i++) {
484                     UserPackage pkg = packages.get(i);
485 
486                     LocationManager locationManager = getSystemServiceSafe(mContext,
487                             LocationManager.class, pkg.user);
488                     if (locationManager.isExtraLocationControllerPackageEnabled() && pkg.pkg.equals(
489                             locationManager.getExtraLocationControllerPackage())) {
490                         packageToNotifyFor = pkg;
491                         break;
492                     }
493                 }
494 
495                 if (packageToNotifyFor == null) {
496                     packageToNotifyFor = packages.get(mRandom.nextInt(packages.size()));
497                 }
498 
499                 try {
500                     pkgInfo = packageToNotifyFor.getPackageInfo();
501                 } catch (PackageManager.NameNotFoundException e) {
502                     packages.remove(packageToNotifyFor);
503                 }
504             }
505             createPermissionReminderChannel(getUserHandleForUid(pkgInfo.applicationInfo.uid));
506             createNotificationForLocationUser(pkgInfo);
507         }
508     }
509 
510     /**
511      * Get the {@link UserPackage packages} which accessed the location
512      *
513      * <p>This also ignores all packages that are excepted from the notification.
514      *
515      * @return The packages we might need to show a notification for
516      * @throws InterruptedException If {@link #mShouldCancel}
517      */
getLocationUsersLocked( @onNull List<PackageOps> allOps)518     private @NonNull List<UserPackage> getLocationUsersLocked(
519             @NonNull List<PackageOps> allOps) throws InterruptedException {
520         List<UserPackage> pkgsWithLocationAccess = new ArrayList<>();
521         List<UserHandle> profiles = mUserManager.getUserProfiles();
522 
523         LocationManager lm = mContext.getSystemService(LocationManager.class);
524 
525         int numPkgs = allOps.size();
526         for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) {
527             PackageOps packageOps = allOps.get(pkgNum);
528 
529             String pkg = packageOps.getPackageName();
530             if (pkg.equals(OS_PKG) || lm.isProviderPackage(pkg)) {
531                 continue;
532             }
533 
534             UserHandle user = getUserHandleForUid(packageOps.getUid());
535             // Do not handle apps that belong to a different profile user group
536             if (!profiles.contains(user)) {
537                 continue;
538             }
539 
540             UserPackage userPkg = new UserPackage(mContext, pkg, user, false);
541             AppPermissionGroup bgLocationGroup = userPkg.getBackgroundLocationGroup();
542             // Do not show notification that do not request the background permission anymore
543             if (bgLocationGroup == null) {
544                 continue;
545             }
546 
547             // Do not show notification that do not currently have the background permission
548             // granted
549             if (!bgLocationGroup.areRuntimePermissionsGranted()) {
550                 continue;
551             }
552 
553             // Do not show notification for permissions that are not user sensitive
554             if (!bgLocationGroup.isUserSensitive()) {
555                 continue;
556             }
557 
558             // Never show notification for pregranted permissions as warning the user via the
559             // notification and then warning the user again when revoking the permission is
560             // confusing
561             if (userPkg.getLocationGroup().hasGrantedByDefaultPermission()
562                     && bgLocationGroup.hasGrantedByDefaultPermission()) {
563                 continue;
564             }
565 
566             int numOps = packageOps.getOps().size();
567             for (int opNum = 0; opNum < numOps; opNum++) {
568                 OpEntry entry = packageOps.getOps().get(opNum);
569 
570                 // To protect against OEM apps that accidentally blame app ops on other packages
571                 // since they can hold the privileged UPDATE_APP_OPS_STATS permission for location
572                 // access in the background we trust only the OS and the location providers. Note
573                 // that this mitigation only handles usage of AppOpsManager#noteProxyOp and not
574                 // direct usage of AppOpsManager#noteOp, i.e. handles bad blaming and not bad
575                 // attribution.
576                 String proxyPackageName = entry.getProxyPackageName();
577                 if (proxyPackageName != null && !proxyPackageName.equals(OS_PKG)
578                         && !lm.isProviderPackage(proxyPackageName)) {
579                     continue;
580                 }
581 
582                 // We show only bg accesses since the location access check feature was enabled
583                 // to handle cases where the feature is remotely toggled since we don't want to
584                 // notify for accesses before the feature was turned on.
585                 long featureEnabledTime = getLocationAccessCheckEnabledTime();
586                 if (featureEnabledTime >= 0 && entry.getLastAccessBackgroundTime(
587                         AppOpsManager.OP_FLAGS_ALL_TRUSTED) >= featureEnabledTime) {
588                     pkgsWithLocationAccess.add(userPkg);
589                     break;
590                 }
591             }
592         }
593         return pkgsWithLocationAccess;
594     }
595 
filterAlreadyNotifiedPackagesLocked( @onNull List<UserPackage> pkgsWithLocationAccess, @NonNull ArraySet<UserPackage> alreadyNotifiedPkgs)596     private void filterAlreadyNotifiedPackagesLocked(
597             @NonNull List<UserPackage> pkgsWithLocationAccess,
598             @NonNull ArraySet<UserPackage> alreadyNotifiedPkgs) throws InterruptedException {
599         resetAlreadyNotifiedPackagesWithoutPermissionLocked(alreadyNotifiedPkgs);
600         pkgsWithLocationAccess.removeAll(alreadyNotifiedPkgs);
601     }
602 
603     /**
604      * Checks whether the location access check feature is enabled and updates the
605      * time when the feature was first enabled. If the feature is enabled and no
606      * enabled time persisted we persist the current time as the enabled time. If
607      * the feature is disabled and an enabled time is persisted we delete the
608      * persisted time.
609      *
610      * @return Whether the location access feature is enabled.
611      */
checkLocationAccessCheckEnabledAndUpdateEnabledTime()612     private boolean checkLocationAccessCheckEnabledAndUpdateEnabledTime() {
613         final long enabledTime = getLocationAccessCheckEnabledTime();
614         if (Utils.isLocationAccessCheckEnabled()) {
615             if (enabledTime <= 0) {
616                 mSharedPrefs.edit().putLong(KEY_LOCATION_ACCESS_CHECK_ENABLED_TIME,
617                         currentTimeMillis()).commit();
618             }
619             return true;
620         } else {
621             if (enabledTime > 0) {
622                 mSharedPrefs.edit().remove(KEY_LOCATION_ACCESS_CHECK_ENABLED_TIME)
623                         .commit();
624             }
625             return false;
626         }
627     }
628 
629     /**
630      * @return The time the location access check was enabled, or 0 if not enabled.
631      */
getLocationAccessCheckEnabledTime()632     private long getLocationAccessCheckEnabledTime() {
633         return mSharedPrefs.getLong(KEY_LOCATION_ACCESS_CHECK_ENABLED_TIME, 0);
634     }
635 
636     /**
637      * Create a notification reminding the user that a package used the location. From this
638      * notification the user can directly go to the screen that allows to change the permission.
639      *
640      * @param pkg The {@link PackageInfo} for the package to to be changed
641      */
createNotificationForLocationUser(@onNull PackageInfo pkg)642     private void createNotificationForLocationUser(@NonNull PackageInfo pkg) {
643         CharSequence pkgLabel = mPackageManager.getApplicationLabel(pkg.applicationInfo);
644 
645         boolean safetyCenterBgLocationReminderEnabled = isSafetyCenterBgLocationReminderEnabled();
646 
647         String pkgName = pkg.packageName;
648         int uid = pkg.applicationInfo.uid;
649         UserHandle user = getUserHandleForUid(uid);
650 
651         NotificationManager notificationManager = getSystemServiceSafe(mContext,
652                 NotificationManager.class, user);
653 
654         long sessionId = INVALID_SESSION_ID;
655         while (sessionId == INVALID_SESSION_ID) {
656             sessionId = new Random().nextLong();
657         }
658 
659         CharSequence appName = Utils.getSettingsLabelForNotifications(mPackageManager);
660 
661         CharSequence notificationTitle =
662                 safetyCenterBgLocationReminderEnabled ? mContext.getString(
663                         R.string.safety_center_background_location_access_notification_title
664                 ) : mContext.getString(
665                         R.string.background_location_access_reminder_notification_title,
666                         pkgLabel);
667 
668         CharSequence notificationContent = safetyCenterBgLocationReminderEnabled
669                 ? mContext.getString(
670                 R.string.safety_center_background_location_access_reminder_notification_content,
671                 pkgLabel) : mContext.getString(
672                 R.string.background_location_access_reminder_notification_content);
673 
674         CharSequence appLabel = appName;
675         Icon smallIcon;
676         int color = mContext.getColor(android.R.color.system_notification_accent_color);
677         if (safetyCenterBgLocationReminderEnabled) {
678             KotlinUtils.NotificationResources notifRes =
679                     KotlinUtils.INSTANCE.getSafetyCenterNotificationResources(mContext);
680             appLabel = notifRes.getAppLabel();
681             smallIcon = notifRes.getSmallIcon();
682             color = notifRes.getColor();
683         } else {
684             smallIcon = Icon.createWithResource(mContext, R.drawable.ic_pin_drop);
685         }
686 
687         Notification.Builder b = (new Notification.Builder(mContext,
688                 PERMISSION_REMINDER_CHANNEL_ID))
689                 .setLocalOnly(true)
690                 .setContentTitle(notificationTitle)
691                 .setContentText(notificationContent)
692                 .setStyle(new Notification.BigTextStyle().bigText(notificationContent))
693                 .setSmallIcon(smallIcon)
694                 .setColor(color)
695                 .setDeleteIntent(createNotificationDismissIntent(pkgName, sessionId, uid))
696                 .setContentIntent(createNotificationClickIntent(pkgName, user, sessionId, uid))
697                 .setAutoCancel(true);
698 
699         if (!safetyCenterBgLocationReminderEnabled) {
700             Drawable pkgIcon = mPackageManager.getApplicationIcon(pkg.applicationInfo);
701             Bitmap pkgIconBmp = createBitmap(pkgIcon.getIntrinsicWidth(),
702                     pkgIcon.getIntrinsicHeight(),
703                     ARGB_8888);
704             Canvas canvas = new Canvas(pkgIconBmp);
705             pkgIcon.setBounds(0, 0, pkgIcon.getIntrinsicWidth(), pkgIcon.getIntrinsicHeight());
706             pkgIcon.draw(canvas);
707             b.setLargeIcon(pkgIconBmp);
708         }
709 
710         if (!TextUtils.isEmpty(appLabel)) {
711             Bundle extras = new Bundle();
712             String appNameSubstitute = appLabel.toString();
713             extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, appNameSubstitute);
714             b.addExtras(extras);
715         }
716 
717         notificationManager.notify(pkgName, LOCATION_ACCESS_CHECK_NOTIFICATION_ID, b.build());
718         markAsNotified(pkgName, user, false);
719 
720         if (DEBUG) Log.i(LOG_TAG, "Notified " + pkgName);
721 
722         Log.v(LOG_TAG, "Location access check notification shown with sessionId=" + sessionId + ""
723                 + " uid=" + pkg.applicationInfo.uid + " pkgName=" + pkgName);
724         if (safetyCenterBgLocationReminderEnabled) {
725             PermissionControllerStatsLog.write(
726                     PRIVACY_SIGNAL_NOTIFICATION_INTERACTION,
727                     PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__BG_LOCATION,
728                     uid,
729                     PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_SHOWN,
730                     sessionId);
731         } else {
732             PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId,
733                     pkg.applicationInfo.uid, pkgName,
734                     LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_PRESENTED);
735         }
736 
737         mSharedPrefs.edit().putLong(KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN,
738                 currentTimeMillis()).apply();
739     }
740 
741     /**
742      * Get currently shown notification. We only ever show one notification per profile group.
743      *
744      * @return The notification or {@code null} if no notification is currently shown
745      */
getCurrentlyShownNotificationLocked()746     private @Nullable StatusBarNotification getCurrentlyShownNotificationLocked() {
747         List<UserHandle> profiles = mUserManager.getUserProfiles();
748 
749         int numProfiles = profiles.size();
750         for (int profileNum = 0; profileNum < numProfiles; profileNum++) {
751             NotificationManager notificationManager;
752             try {
753                 notificationManager = getSystemServiceSafe(mContext, NotificationManager.class,
754                         profiles.get(profileNum));
755             } catch (IllegalStateException e) {
756                 continue;
757             }
758 
759             StatusBarNotification[] notifications = notificationManager.getActiveNotifications();
760 
761             int numNotifications = notifications.length;
762             for (int notificationNum = 0; notificationNum < numNotifications; notificationNum++) {
763                 StatusBarNotification notification = notifications[notificationNum];
764                 if (notification.getId() == LOCATION_ACCESS_CHECK_NOTIFICATION_ID
765                         && notification.getUser() != null && notification.getTag() != null) {
766                     return notification;
767                 }
768             }
769         }
770         return null;
771     }
772 
773     /**
774      * Go through the list of packages we already shown a notification for and remove those that do
775      * not request fine background location access.
776      *
777      * @param alreadyNotifiedPkgs The packages we already shown a notification for. This parameter
778      *                            is modified inside of this method.
779      * @throws InterruptedException If {@link #mShouldCancel}
780      */
resetAlreadyNotifiedPackagesWithoutPermissionLocked( @onNull ArraySet<UserPackage> alreadyNotifiedPkgs)781     private void resetAlreadyNotifiedPackagesWithoutPermissionLocked(
782             @NonNull ArraySet<UserPackage> alreadyNotifiedPkgs) throws InterruptedException {
783         ArrayList<UserPackage> packagesToRemove = new ArrayList<>();
784 
785         for (UserPackage userPkg : alreadyNotifiedPkgs) {
786             throwInterruptedExceptionIfTaskIsCanceled();
787             AppPermissionGroup bgLocationGroup = userPkg.getBackgroundLocationGroup();
788             if (bgLocationGroup == null || !bgLocationGroup.areRuntimePermissionsGranted()) {
789                 packagesToRemove.add(userPkg);
790             }
791         }
792 
793         if (!packagesToRemove.isEmpty()) {
794             alreadyNotifiedPkgs.removeAll(packagesToRemove);
795             persistAlreadyNotifiedPackagesLocked(alreadyNotifiedPkgs);
796             throwInterruptedExceptionIfTaskIsCanceled();
797         }
798     }
799 
800     /**
801      * Remove all persisted state for a package.
802      *
803      * @param pkg  name of package
804      * @param user user the package belongs to
805      */
forgetAboutPackage(@onNull String pkg, @NonNull UserHandle user)806     private void forgetAboutPackage(@NonNull String pkg, @NonNull UserHandle user) {
807         synchronized (sLock) {
808             StatusBarNotification notification = getCurrentlyShownNotificationLocked();
809             if (notification != null && notification.getUser().equals(user)
810                     && notification.getTag().equals(pkg)) {
811                 getSystemServiceSafe(mContext, NotificationManager.class, user).cancel(
812                         pkg, LOCATION_ACCESS_CHECK_NOTIFICATION_ID);
813             }
814 
815             ArraySet<UserPackage> packages = loadAlreadyNotifiedPackagesLocked();
816             packages.remove(new UserPackage(mContext, pkg, user, false));
817             persistAlreadyNotifiedPackagesLocked(packages);
818         }
819     }
820 
821     /**
822      * After a small delay schedule a check if we should show a notification.
823      *
824      * <p>This is called when location access is granted to an app. In this case it is likely that
825      * the app will access the location soon. If this happens the notification will appear only a
826      * little after the user granted the location.
827      */
checkLocationAccessSoon()828     public void checkLocationAccessSoon() {
829         JobInfo.Builder b = (new JobInfo.Builder(LOCATION_ACCESS_CHECK_JOB_ID,
830                 new ComponentName(mContext, LocationAccessCheckJobService.class)))
831                 .setMinimumLatency(getDelayMillis());
832 
833         int scheduleResult = mJobScheduler.schedule(b.build());
834         if (scheduleResult != RESULT_SUCCESS) {
835             Log.e(LOG_TAG, "Could not schedule location access check " + scheduleResult);
836         }
837     }
838 
839     /**
840      * Cancel the background access warning notification for an app if the permission has been
841      * revoked for the app and forget persisted information about the app
842      */
cancelBackgroundAccessWarningNotification(String packageName, UserHandle user, Boolean forgetAboutPackage)843     public void cancelBackgroundAccessWarningNotification(String packageName, UserHandle user,
844             Boolean forgetAboutPackage) {
845         // Cancel the current notification if background
846         // location access for the package is revoked
847         StatusBarNotification notification = getCurrentlyShownNotificationLocked();
848         if (notification != null && notification.getUser().equals(user)
849                 && notification.getTag().equals(packageName)) {
850             getSystemServiceSafe(mContext, NotificationManager.class, user).cancel(
851                     packageName, LOCATION_ACCESS_CHECK_NOTIFICATION_ID);
852         }
853 
854         if (isSafetyCenterBgLocationReminderEnabled()) {
855             rescanAndPushSafetyCenterData(new SafetyEvent.Builder(
856                     SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED)
857                     .build(), user);
858         }
859 
860         if (forgetAboutPackage) {
861             forgetAboutPackage(packageName, user);
862         }
863     }
864 
865     /**
866      * Cancel the background access warning notification if currently being shown
867      */
cancelBackgroundAccessWarningNotification()868     public void cancelBackgroundAccessWarningNotification() {
869         StatusBarNotification notification = getCurrentlyShownNotificationLocked();
870         if (notification != null) {
871             getSystemServiceSafe(mContext, NotificationManager.class,
872                     notification.getUser()).cancel(
873                     notification.getTag(), LOCATION_ACCESS_CHECK_NOTIFICATION_ID);
874         }
875     }
876 
877     @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
isSafetyCenterBgLocationReminderEnabled()878     private boolean isSafetyCenterBgLocationReminderEnabled() {
879         if (!SdkLevel.isAtLeastT()) {
880             return false;
881         }
882 
883         return DeviceConfig.getBoolean(
884                 DeviceConfig.NAMESPACE_PRIVACY,
885                 PROPERTY_BG_LOCATION_CHECK_ENABLED, true)
886                 && getSystemServiceSafe(mContext,
887                 SafetyCenterManager.class).isSafetyCenterEnabled();
888     }
889 
890     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
sendToSafetyCenter(List<UserPackage> userPackages, SafetyEvent safetyEvent, @Nullable ArraySet<UserPackage> alreadyNotifiedPackages, @Nullable UserHandle user)891     private void sendToSafetyCenter(List<UserPackage> userPackages, SafetyEvent safetyEvent,
892             @Nullable ArraySet<UserPackage> alreadyNotifiedPackages, @Nullable UserHandle user) {
893         try {
894             Set<UserPackage> alreadyDismissedPackages =
895                     getAlreadyDismissedPackages(alreadyNotifiedPackages);
896 
897             // Filter out packages already dismissed by the user in safety center
898             List<UserPackage> filteredPackages = userPackages.stream().filter(
899                     pkg -> !alreadyDismissedPackages.contains(pkg)).collect(
900                     Collectors.toList());
901 
902             Map<UserHandle, List<UserPackage>> userHandleToUserPackagesMap =
903                     splitUserPackageByUserHandle(filteredPackages);
904 
905             if (user == null) {
906                 // Get all the user profiles
907                 List<UserHandle> userProfiles = mUserManager.getUserProfiles();
908                 for (UserHandle userProfile : userProfiles) {
909                     sendUserDataToSafetyCenter(userHandleToUserPackagesMap.getOrDefault(userProfile,
910                             new ArrayList<>()), safetyEvent, userProfile);
911                 }
912             } else {
913                 sendUserDataToSafetyCenter(userHandleToUserPackagesMap.getOrDefault(user,
914                         new ArrayList<>()), safetyEvent, user);
915             }
916 
917         } catch (Exception e) {
918             Log.e(LOG_TAG, "Could not send to safety center", e);
919         }
920     }
921 
getAlreadyDismissedPackages( @ullable ArraySet<UserPackage> alreadyNotifiedPackages)922     private Set<UserPackage> getAlreadyDismissedPackages(
923             @Nullable ArraySet<UserPackage> alreadyNotifiedPackages) {
924         if (alreadyNotifiedPackages == null) {
925             alreadyNotifiedPackages = loadAlreadyNotifiedPackagesLocked();
926         }
927         return alreadyNotifiedPackages.stream().filter(
928                 pkg -> pkg.dismissedInSafetyCenter).collect(
929                 Collectors.toSet());
930     }
931 
932     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
splitUserPackageByUserHandle( List<UserPackage> userPackages)933     private Map<UserHandle, List<UserPackage>> splitUserPackageByUserHandle(
934             List<UserPackage> userPackages) {
935         Map<UserHandle, List<UserPackage>> userHandleToUserPackagesMap = new ArrayMap<>();
936         for (UserPackage userPackage : userPackages) {
937             if (userHandleToUserPackagesMap.get(userPackage.user) == null) {
938                 userHandleToUserPackagesMap.put(userPackage.user, new ArrayList<>());
939             }
940             userHandleToUserPackagesMap.get(userPackage.user).add(userPackage);
941         }
942         return userHandleToUserPackagesMap;
943     }
944 
945     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
sendUserDataToSafetyCenter(List<UserPackage> userPackages, SafetyEvent safetyEvent, @Nullable UserHandle user)946     private void sendUserDataToSafetyCenter(List<UserPackage> userPackages,
947             SafetyEvent safetyEvent, @Nullable UserHandle user) {
948         SafetySourceData.Builder safetySourceDataBuilder = new SafetySourceData.Builder();
949         Context userContext = null;
950         for (UserPackage userPkg : userPackages) {
951             if (userContext == null) {
952                 userContext = userPkg.mContext;
953             }
954             SafetySourceIssue sourceIssue = createSafetySourceIssue(userPkg);
955             if (sourceIssue != null) {
956                 safetySourceDataBuilder.addIssue(sourceIssue);
957             }
958         }
959         if (userContext == null && user != null) {
960             userContext = mContext.createContextAsUser(user, 0);
961         }
962         if (userContext != null) {
963             getSystemServiceSafe(userContext, SafetyCenterManager.class).setSafetySourceData(
964                     BG_LOCATION_SOURCE_ID,
965                     safetySourceDataBuilder.build(),
966                     safetyEvent
967             );
968         }
969     }
970 
971     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
createSafetySourceIssue(UserPackage userPackage)972     private SafetySourceIssue createSafetySourceIssue(UserPackage userPackage) {
973         PackageInfo pkgInfo = null;
974         try {
975             pkgInfo = userPackage.getPackageInfo();
976         } catch (PackageManager.NameNotFoundException e) {
977             Log.e(LOG_TAG, "Could not get package info for " + userPackage, e);
978             return null;
979         }
980 
981         long sessionId = INVALID_SESSION_ID;
982         while (sessionId == INVALID_SESSION_ID) {
983             sessionId = new Random().nextLong();
984         }
985 
986         int uid = pkgInfo.applicationInfo.uid;
987 
988         Intent primaryActionIntent = new Intent(mContext, SafetyCenterPrimaryActionHandler.class);
989         primaryActionIntent.putExtra(EXTRA_PACKAGE_NAME, userPackage.pkg);
990         primaryActionIntent.putExtra(EXTRA_USER, userPackage.user);
991         primaryActionIntent.putExtra(EXTRA_UID, uid);
992         primaryActionIntent.putExtra(EXTRA_SESSION_ID, sessionId);
993         primaryActionIntent.setFlags(FLAG_RECEIVER_FOREGROUND);
994         primaryActionIntent.setIdentifier(userPackage.pkg + userPackage.user);
995 
996         PendingIntent revokeIntent = PendingIntent.getBroadcast(mContext, 0,
997                 primaryActionIntent,
998                 FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE);
999 
1000         Action revokeAction = new Action.Builder(createLocationRevokeActionId(userPackage.pkg,
1001                 userPackage.user),
1002                 mContext.getString(R.string.permission_access_only_foreground),
1003                 revokeIntent).setWillResolve(true).setSuccessMessage(mContext.getString(
1004                 R.string.safety_center_background_location_access_revoked)).build();
1005 
1006         Intent secondaryActionIntent = new Intent(Intent.ACTION_REVIEW_PERMISSION_HISTORY);
1007         secondaryActionIntent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, LOCATION);
1008 
1009         PendingIntent locationUsageIntent = PendingIntent.getActivity(mContext, 0,
1010                 secondaryActionIntent,
1011                 FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE);
1012 
1013         Action viewLocationUsageAction = new Action.Builder(VIEW_LOCATION_ACCESS_ID,
1014                 mContext.getString(R.string.safety_center_view_recent_location_access),
1015                 locationUsageIntent).build();
1016 
1017         String pkgName = userPackage.pkg;
1018         String id = createSafetySourceIssueId(pkgName, userPackage.user);
1019 
1020         CharSequence pkgLabel = mPackageManager.getApplicationLabel(pkgInfo.applicationInfo);
1021 
1022         return new SafetySourceIssue.Builder(
1023                 id,
1024                 mContext.getString(
1025                         R.string.safety_center_background_location_access_reminder_title),
1026                 mContext.getString(
1027                         R.string.safety_center_background_location_access_reminder_summary),
1028                 SafetySourceData.SEVERITY_LEVEL_INFORMATION,
1029                 ISSUE_TYPE_ID)
1030                 .setSubtitle(pkgLabel)
1031                 .addAction(revokeAction)
1032                 .addAction(viewLocationUsageAction)
1033                 .setOnDismissPendingIntent(
1034                         createWarningCardDismissalIntent(pkgName, sessionId, uid))
1035                 .setIssueCategory(SafetySourceIssue.ISSUE_CATEGORY_DEVICE)
1036                 .build();
1037     }
1038 
createNotificationDismissIntent(String pkgName, long sessionId, int uid)1039     private PendingIntent createNotificationDismissIntent(String pkgName, long sessionId, int uid) {
1040         Intent dismissIntent = new Intent(mContext, NotificationDeleteHandler.class);
1041         dismissIntent.putExtra(EXTRA_PACKAGE_NAME, pkgName);
1042         dismissIntent.putExtra(EXTRA_SESSION_ID, sessionId);
1043         dismissIntent.putExtra(EXTRA_UID, uid);
1044         UserHandle user = getUserHandleForUid(uid);
1045         dismissIntent.putExtra(EXTRA_USER, user);
1046         dismissIntent.setIdentifier(pkgName + user);
1047         dismissIntent.setFlags(FLAG_RECEIVER_FOREGROUND);
1048         return PendingIntent.getBroadcast(mContext, 0, dismissIntent,
1049                 FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE);
1050     }
1051 
createNotificationClickIntent(String pkg, UserHandle user, long sessionId, int uid)1052     private PendingIntent createNotificationClickIntent(String pkg, UserHandle user,
1053             long sessionId, int uid) {
1054         Intent clickIntent = null;
1055         if (isSafetyCenterBgLocationReminderEnabled()) {
1056             clickIntent = new Intent(ACTION_SAFETY_CENTER);
1057         } else {
1058             clickIntent = new Intent(ACTION_MANAGE_APP_PERMISSION);
1059             clickIntent.putExtra(EXTRA_PERMISSION_GROUP_NAME, LOCATION);
1060         }
1061         clickIntent.putExtra(EXTRA_PACKAGE_NAME, pkg);
1062         clickIntent.putExtra(EXTRA_USER, user);
1063         clickIntent.putExtra(EXTRA_SESSION_ID, sessionId);
1064         clickIntent.putExtra(EXTRA_UID, uid);
1065         clickIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK);
1066         return PendingIntent.getActivity(mContext, 0, clickIntent,
1067                 FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE);
1068     }
1069 
createWarningCardDismissalIntent(String pkgName, long sessionId, int uid)1070     private PendingIntent createWarningCardDismissalIntent(String pkgName, long sessionId,
1071             int uid) {
1072         Intent dismissIntent = new Intent(mContext, WarningCardDismissalHandler.class);
1073         dismissIntent.putExtra(EXTRA_PACKAGE_NAME, pkgName);
1074         dismissIntent.putExtra(EXTRA_SESSION_ID, sessionId);
1075         dismissIntent.putExtra(EXTRA_UID, uid);
1076         UserHandle user = getUserHandleForUid(uid);
1077         dismissIntent.putExtra(EXTRA_USER, user);
1078         dismissIntent.setIdentifier(pkgName + user);
1079         dismissIntent.setFlags(FLAG_RECEIVER_FOREGROUND);
1080         return PendingIntent.getBroadcast(mContext, 0, dismissIntent,
1081                 FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE);
1082     }
1083 
1084     /**
1085      * Check if the current user is the profile parent.
1086      *
1087      * @return {@code true} if the current user is the profile parent.
1088      */
isRunningInParentProfile()1089     private boolean isRunningInParentProfile() {
1090         UserHandle user = UserHandle.of(myUserId());
1091         UserHandle parent = mUserManager.getProfileParent(user);
1092 
1093         return parent == null || user.equals(parent);
1094     }
1095 
1096     /**
1097      * Query for packages having background location access and push to safety center
1098      *
1099      * @param safetyEvent Safety event for which data is being pushed
1100      * @param user Optional, if supplied only send safety center data for that user
1101      */
1102     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
rescanAndPushSafetyCenterData(SafetyEvent safetyEvent, @Nullable UserHandle user)1103     public void rescanAndPushSafetyCenterData(SafetyEvent safetyEvent, @Nullable UserHandle user) {
1104         if (!isSafetyCenterBgLocationReminderEnabled()) {
1105             return;
1106         }
1107         try {
1108             List<UserPackage> packages = getLocationUsersLocked(mAppOpsManager.getPackagesForOps(
1109                     new String[]{OPSTR_FINE_LOCATION}));
1110             sendToSafetyCenter(packages, safetyEvent, null, user);
1111         } catch (InterruptedException e) {
1112             Log.e(LOG_TAG, "Couldn't get ops for location");
1113         }
1114     }
1115 
1116     /**
1117      * On boot set up a periodic job that starts checks.
1118      */
1119     public static class SetupPeriodicBackgroundLocationAccessCheck extends BroadcastReceiver {
1120         @Override
onReceive(Context context, Intent intent)1121         public void onReceive(Context context, Intent intent) {
1122             LocationAccessCheck locationAccessCheck = new LocationAccessCheck(context, null);
1123             JobScheduler jobScheduler = getSystemServiceSafe(context, JobScheduler.class);
1124 
1125             if (!locationAccessCheck.isRunningInParentProfile()) {
1126                 // Profile parent handles child profiles too.
1127                 return;
1128             }
1129 
1130             // Init LocationAccessCheckEnabledTime if needed
1131             locationAccessCheck.checkLocationAccessCheckEnabledAndUpdateEnabledTime();
1132 
1133             if (jobScheduler.getPendingJob(PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID) == null) {
1134                 JobInfo.Builder b = (new JobInfo.Builder(PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID,
1135                         new ComponentName(context, LocationAccessCheckJobService.class)))
1136                         .setPeriodic(locationAccessCheck.getPeriodicCheckIntervalMillis(),
1137                                 locationAccessCheck.getFlexForPeriodicCheckMillis());
1138 
1139                 int scheduleResult = jobScheduler.schedule(b.build());
1140                 if (scheduleResult != RESULT_SUCCESS) {
1141                     Log.e(LOG_TAG, "Could not schedule periodic location access check "
1142                             + scheduleResult);
1143                 }
1144             }
1145         }
1146     }
1147 
1148     /**
1149      * Checks if a new notification should be shown.
1150      */
1151     public static class LocationAccessCheckJobService extends JobService {
1152         private LocationAccessCheck mLocationAccessCheck;
1153 
1154         /**
1155          * If we currently check if we should show a notification, the task executing the check
1156          */
1157         // @GuardedBy("sLock")
1158         private @Nullable AddLocationNotificationIfNeededTask mAddLocationNotificationIfNeededTask;
1159 
1160         @Override
onCreate()1161         public void onCreate() {
1162             Log.v(LOG_TAG, "LocationAccessCheck privacy job is created");
1163             super.onCreate();
1164             mLocationAccessCheck = new LocationAccessCheck(this, () -> {
1165                 synchronized (sLock) {
1166                     AddLocationNotificationIfNeededTask task = mAddLocationNotificationIfNeededTask;
1167 
1168                     return task != null && task.isCancelled();
1169                 }
1170             });
1171         }
1172 
1173         /**
1174          * Starts an asynchronous check if a location access notification should be shown.
1175          *
1176          * @param params Not used other than for interacting with job scheduling
1177          * @return {@code false} iff another check if already running
1178          */
1179         @Override
onStartJob(JobParameters params)1180         public boolean onStartJob(JobParameters params) {
1181             Log.v(LOG_TAG, "LocationAccessCheck privacy job is started");
1182             synchronized (LocationAccessCheck.sLock) {
1183                 if (mAddLocationNotificationIfNeededTask != null) {
1184                     Log.v(LOG_TAG, "LocationAccessCheck old job not completed yet.");
1185                     return false;
1186                 }
1187 
1188                 mAddLocationNotificationIfNeededTask =
1189                         new AddLocationNotificationIfNeededTask();
1190 
1191                 mAddLocationNotificationIfNeededTask.execute(params, this);
1192             }
1193 
1194             return true;
1195         }
1196 
1197         /**
1198          * Abort the check if still running.
1199          *
1200          * @param params ignored
1201          * @return false
1202          */
1203         @Override
onStopJob(JobParameters params)1204         public boolean onStopJob(JobParameters params) {
1205             Log.v(LOG_TAG, "LocationAccessCheck privacy source onStopJob called.");
1206             AddLocationNotificationIfNeededTask task;
1207             synchronized (sLock) {
1208                 if (mAddLocationNotificationIfNeededTask == null) {
1209                     return false;
1210                 } else {
1211                     task = mAddLocationNotificationIfNeededTask;
1212                 }
1213             }
1214 
1215             task.cancel(false);
1216 
1217             try {
1218                 // Wait for task to finish
1219                 task.get();
1220             } catch (Exception e) {
1221                 Log.e(LOG_TAG, "While waiting for " + task + " to finish", e);
1222             }
1223 
1224             return false;
1225         }
1226 
1227         /**
1228          * A {@link AsyncTask task} that runs the check in the background.
1229          */
1230         private class AddLocationNotificationIfNeededTask extends
1231                 AsyncTask<Object, Void, Void> {
1232             @Override
doInBackground(Object... in)1233             protected final Void doInBackground(Object... in) {
1234                 JobParameters params = (JobParameters) in[0];
1235                 LocationAccessCheckJobService service = (LocationAccessCheckJobService) in[1];
1236                 mLocationAccessCheck.addLocationNotificationIfNeeded(params, service);
1237                 return null;
1238             }
1239         }
1240     }
1241 
1242     /**
1243      * Handle the case where the notification is swiped away without further interaction.
1244      */
1245     public static class NotificationDeleteHandler extends BroadcastReceiver {
1246         @Override
onReceive(Context context, Intent intent)1247         public void onReceive(Context context, Intent intent) {
1248             String pkg = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME);
1249             UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER);
1250             long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID);
1251             int uid = intent.getIntExtra(EXTRA_UID, -1);
1252 
1253             Log.v(LOG_TAG,
1254                     "Location access check notification declined with sessionId=" + sessionId + ""
1255                             + " uid=" + uid + " pkgName=" + pkg);
1256             LocationAccessCheck locationAccessCheck = new LocationAccessCheck(context, null);
1257 
1258             if (locationAccessCheck.isSafetyCenterBgLocationReminderEnabled()) {
1259                 PermissionControllerStatsLog.write(
1260                         PRIVACY_SIGNAL_NOTIFICATION_INTERACTION,
1261                         PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__BG_LOCATION,
1262                         uid,
1263                         PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__DISMISSED,
1264                         sessionId
1265                 );
1266             } else {
1267                 PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION,
1268                         sessionId,
1269                         uid, pkg,
1270                         LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_DECLINED);
1271             }
1272             locationAccessCheck.markAsNotified(pkg, user, false);
1273         }
1274     }
1275 
1276     /**
1277      * Broadcast receiver to handle the primary action from a safety center warning card
1278      */
1279     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
1280     public static class SafetyCenterPrimaryActionHandler extends BroadcastReceiver {
1281         @Override
onReceive(Context context, Intent intent)1282         public void onReceive(Context context, Intent intent) {
1283             String packageName = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME);
1284             UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER);
1285             int uid = intent.getIntExtra(EXTRA_UID, -1);
1286             long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID);
1287             // Revoke bg location permission and notify safety center
1288             KotlinUtils.INSTANCE.revokeBackgroundRuntimePermissions(context, packageName, LOCATION,
1289                     user, () -> {
1290                         new LocationAccessCheck(context, null).rescanAndPushSafetyCenterData(
1291                                 new SafetyEvent.Builder(
1292                                         SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED)
1293                                         .setSafetySourceIssueId(
1294                                                 createSafetySourceIssueId(packageName, user))
1295                                         .setSafetySourceIssueActionId(
1296                                                 createLocationRevokeActionId(packageName, user))
1297                                         .build(), user);
1298                     });
1299             PermissionControllerStatsLog.write(
1300                     PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION,
1301                     PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__BG_LOCATION,
1302                     uid,
1303                     PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CLICKED_CTA1,
1304                     sessionId
1305             );
1306 
1307         }
1308     }
1309 
createSafetySourceIssueId(String packageName, UserHandle user)1310     private static String createSafetySourceIssueId(String packageName, UserHandle user) {
1311         return ISSUE_ID_PREFIX + packageName + user;
1312     }
1313 
createLocationRevokeActionId(String packageName, UserHandle user)1314     private static String createLocationRevokeActionId(String packageName, UserHandle user) {
1315         return REVOKE_LOCATION_ACCESS_ID_PREFIX + packageName + user;
1316     }
1317 
1318     /**
1319      * Handle the case where the warning card is dismissed by the user in Safety center
1320      */
1321     public static class WarningCardDismissalHandler extends BroadcastReceiver {
1322         @Override
onReceive(Context context, Intent intent)1323         public void onReceive(Context context, Intent intent) {
1324             String pkg = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME);
1325             UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER);
1326             long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID);
1327             int uid = intent.getIntExtra(EXTRA_UID, -1);
1328             Log.v(LOG_TAG,
1329                     "Location access check warning card dismissed with sessionId=" + sessionId + ""
1330                             + " uid=" + uid + " pkgName=" + pkg);
1331             PermissionControllerStatsLog.write(
1332                     PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION,
1333                     PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__BG_LOCATION,
1334                     uid,
1335                     PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CARD_DISMISSED,
1336                     sessionId
1337             );
1338 
1339             LocationAccessCheck locationAccessCheck = new LocationAccessCheck(context, null);
1340             locationAccessCheck.markAsNotified(pkg, user, true);
1341             locationAccessCheck.cancelBackgroundAccessWarningNotification(pkg, user, false);
1342         }
1343     }
1344 
1345     /**
1346      * If a package gets removed or the data of the package gets cleared, forget that we showed a
1347      * notification for it.
1348      */
1349     public static class PackageResetHandler extends BroadcastReceiver {
1350         @Override
onReceive(Context context, Intent intent)1351         public void onReceive(Context context, Intent intent) {
1352             String action = intent.getAction();
1353             if (!(Objects.equals(action, Intent.ACTION_PACKAGE_DATA_CLEARED)
1354                     || Objects.equals(action, Intent.ACTION_PACKAGE_FULLY_REMOVED))) {
1355                 return;
1356             }
1357 
1358             Uri data = Preconditions.checkNotNull(intent.getData());
1359             UserHandle user = getUserHandleForUid(intent.getIntExtra(EXTRA_UID, 0));
1360             if (DEBUG) Log.i(LOG_TAG, "Reset " + data.getSchemeSpecificPart());
1361             LocationAccessCheck locationAccessCheck = new LocationAccessCheck(context, null);
1362             String packageName =  data.getSchemeSpecificPart();
1363             locationAccessCheck.forgetAboutPackage(packageName, user);
1364             if (locationAccessCheck.isSafetyCenterBgLocationReminderEnabled()) {
1365                 locationAccessCheck.rescanAndPushSafetyCenterData(
1366                         new SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED)
1367                                 .build(), user);
1368             }
1369         }
1370     }
1371 
1372     /**
1373      * A immutable class containing a package name and a {@link UserHandle}.
1374      */
1375     private static final class UserPackage {
1376         private final @NonNull Context mContext;
1377 
1378         public final @NonNull String pkg;
1379         public final @NonNull UserHandle user;
1380         public final boolean dismissedInSafetyCenter;
1381 
1382         /**
1383          * Create a new {@link UserPackage}
1384          *
1385          * @param context               A context to be used by methods of this object
1386          * @param pkg                   The name of the package
1387          * @param user                  The user the package belongs to
1388          * @param dismissedInSafetyCenter Optional boolean recording if the safety center
1389          *                                       warning was dismissed by the user
1390          */
UserPackage(@onNull Context context, @NonNull String pkg, @NonNull UserHandle user, boolean dismissedInSafetyCenter)1391         UserPackage(@NonNull Context context, @NonNull String pkg, @NonNull UserHandle user,
1392                 boolean dismissedInSafetyCenter) {
1393             try {
1394                 mContext = context.createPackageContextAsUser(context.getPackageName(), 0, user);
1395             } catch (PackageManager.NameNotFoundException e) {
1396                 throw new IllegalStateException(e);
1397             }
1398 
1399             this.pkg = pkg;
1400             this.user = user;
1401             this.dismissedInSafetyCenter = dismissedInSafetyCenter;
1402         }
1403 
1404         /**
1405          * Get {@link PackageInfo} for this user package.
1406          *
1407          * @return The package info
1408          * @throws PackageManager.NameNotFoundException if package/user does not exist
1409          */
1410         @NonNull
getPackageInfo()1411         PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
1412             return mContext.getPackageManager().getPackageInfo(pkg, GET_PERMISSIONS);
1413         }
1414 
1415         /**
1416          * Get the {@link AppPermissionGroup} for
1417          * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and this user package.
1418          *
1419          * @return The app permission group or {@code null} if the app does not request location
1420          */
1421         @Nullable
getLocationGroup()1422         AppPermissionGroup getLocationGroup() {
1423             try {
1424                 return AppPermissionGroup.create(mContext, getPackageInfo(), ACCESS_FINE_LOCATION,
1425                         false);
1426             } catch (PackageManager.NameNotFoundException e) {
1427                 return null;
1428             }
1429         }
1430 
1431         /**
1432          * Get the {@link AppPermissionGroup} for the background location of
1433          * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and this user package.
1434          *
1435          * @return The app permission group or {@code null} if the app does not request background
1436          * location
1437          */
1438         @Nullable
getBackgroundLocationGroup()1439         AppPermissionGroup getBackgroundLocationGroup() {
1440             AppPermissionGroup locationGroup = getLocationGroup();
1441             if (locationGroup == null) {
1442                 return null;
1443             }
1444 
1445             return locationGroup.getBackgroundPermissions();
1446         }
1447 
1448         @Override
equals(Object o)1449         public boolean equals(Object o) {
1450             if (!(o instanceof UserPackage)) {
1451                 return false;
1452             }
1453 
1454             UserPackage userPackage = (UserPackage) o;
1455             return pkg.equals(userPackage.pkg) && user.equals(userPackage.user);
1456         }
1457 
1458         @Override
hashCode()1459         public int hashCode() {
1460             return Objects.hash(pkg, user);
1461         }
1462 
1463         @Override
toString()1464         public String toString() {
1465             return "UserPackage { "
1466                     + "pkg = " + pkg + ", "
1467                     + "UserHandle = " + user.toString() + ", "
1468                     + "dismissedInSafetyCenter = " + dismissedInSafetyCenter + " }";
1469         }
1470     }
1471 }
1472