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