| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.packageinstaller.permission.service; |
| |
| import static android.Manifest.permission.ACCESS_FINE_LOCATION; |
| import static android.app.AppOpsManager.OPSTR_FINE_LOCATION; |
| import static android.app.NotificationManager.IMPORTANCE_HIGH; |
| import static android.app.PendingIntent.FLAG_ONE_SHOT; |
| import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; |
| import static android.app.PendingIntent.getBroadcast; |
| import static android.app.job.JobScheduler.RESULT_SUCCESS; |
| import static android.content.Context.MODE_PRIVATE; |
| import static android.content.Intent.EXTRA_PACKAGE_NAME; |
| import static android.content.Intent.EXTRA_PERMISSION_NAME; |
| import static android.content.Intent.EXTRA_UID; |
| import static android.content.Intent.EXTRA_USER; |
| import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; |
| import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; |
| import static android.content.pm.PackageManager.GET_PERMISSIONS; |
| import static android.graphics.Bitmap.Config.ARGB_8888; |
| import static android.graphics.Bitmap.createBitmap; |
| import static android.os.UserHandle.getUserHandleForUid; |
| import static android.os.UserHandle.myUserId; |
| import static android.provider.Settings.Secure.LOCATION_ACCESS_CHECK_DELAY_MILLIS; |
| import static android.provider.Settings.Secure.LOCATION_ACCESS_CHECK_INTERVAL_MILLIS; |
| |
| import static com.android.packageinstaller.Constants.KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN; |
| import static com.android.packageinstaller.Constants.LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE; |
| import static com.android.packageinstaller.Constants.LOCATION_ACCESS_CHECK_JOB_ID; |
| import static com.android.packageinstaller.Constants.LOCATION_ACCESS_CHECK_NOTIFICATION_ID; |
| import static com.android.packageinstaller.Constants.PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID; |
| import static com.android.packageinstaller.Constants.PERMISSION_REMINDER_CHANNEL_ID; |
| import static com.android.packageinstaller.Constants.PREFERENCES_FILE; |
| import static com.android.packageinstaller.permission.utils.Utils.OS_PKG; |
| import static com.android.packageinstaller.permission.utils.Utils.getGroupOfPlatformPermission; |
| import static com.android.packageinstaller.permission.utils.Utils.getParcelableExtraSafe; |
| import static com.android.packageinstaller.permission.utils.Utils.getStringExtraSafe; |
| import static com.android.packageinstaller.permission.utils.Utils.getSystemServiceSafe; |
| import static com.android.packageinstaller.permission.utils.Utils.isLocationAccessCheckEnabled; |
| |
| import static java.lang.System.currentTimeMillis; |
| import static java.util.concurrent.TimeUnit.DAYS; |
| import static java.util.concurrent.TimeUnit.MINUTES; |
| |
| import android.app.AppOpsManager; |
| import android.app.AppOpsManager.HistoricalOps; |
| import android.app.AppOpsManager.HistoricalOpsRequest; |
| import android.app.AppOpsManager.HistoricalPackageOps; |
| import android.app.Notification; |
| import android.app.NotificationChannel; |
| import android.app.NotificationManager; |
| import android.app.job.JobInfo; |
| import android.app.job.JobParameters; |
| import android.app.job.JobScheduler; |
| import android.app.job.JobService; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.drawable.Drawable; |
| import android.location.LocationManager; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.provider.Settings; |
| import android.service.notification.StatusBarNotification; |
| import android.util.ArraySet; |
| import android.util.Log; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.WorkerThread; |
| import androidx.core.util.Preconditions; |
| |
| import com.android.packageinstaller.permission.model.AppPermissionGroup; |
| import com.android.packageinstaller.permission.ui.AppPermissionActivity; |
| import com.android.packageinstaller.permission.utils.CollectionUtils; |
| import com.android.permissioncontroller.R; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.OutputStreamWriter; |
| import java.time.Instant; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Random; |
| import java.util.function.BooleanSupplier; |
| |
| /** |
| * Show notification that double-guesses the user if she/he really wants to grant fine background |
| * location access to an app. |
| * |
| * <p>A notification is scheduled after the background permission access is granted via |
| * {@link #checkLocationAccessSoon()} or periodically. |
| * |
| * <p>We rate limit the number of notification we show and only ever show one notification at a |
| * time. Further we only shown notifications if the app has actually accessed the fine location |
| * in the background. |
| * |
| * <p>As there are many cases why a notification should not been shown, we always schedule a |
| * {@link #addLocationNotificationIfNeeded check} which then might add a notification. |
| */ |
| public class LocationAccessCheck { |
| private static final String LOG_TAG = LocationAccessCheck.class.getSimpleName(); |
| private static final boolean DEBUG = false; |
| |
| /** Lock required for all methods called {@code ...Locked} */ |
| private static final Object sLock = new Object(); |
| |
| private final Random mRandom = new Random(); |
| |
| private final @NonNull Context mContext; |
| private final @NonNull JobScheduler mJobScheduler; |
| private final @NonNull ContentResolver mContentResolver; |
| private final @NonNull AppOpsManager mAppOpsManager; |
| private final @NonNull PackageManager mPackageManager; |
| private final @NonNull UserManager mUserManager; |
| private final @NonNull SharedPreferences mSharedPrefs; |
| |
| /** If the current long running operation should be canceled */ |
| private final @Nullable BooleanSupplier mShouldCancel; |
| |
| /** |
| * Get time in between two periodic checks. |
| * |
| * <p>Default: 1 day |
| * |
| * @return The time in between check in milliseconds |
| */ |
| private long getPeriodicCheckIntervalMillis() { |
| return Settings.Secure.getLong(mContentResolver, |
| LOCATION_ACCESS_CHECK_INTERVAL_MILLIS, DAYS.toMillis(1)); |
| } |
| |
| /** |
| * Flexibility of the periodic check. |
| * |
| * <p>10% of {@link #getPeriodicCheckIntervalMillis()} |
| * |
| * @return The flexibility of the periodic check in milliseconds |
| */ |
| private long getFlexForPeriodicCheckMillis() { |
| return getPeriodicCheckIntervalMillis() / 10; |
| } |
| |
| /** |
| * Get the delay in between granting a permission and the follow up check. |
| * |
| * <p>Default: 10 minutes |
| * |
| * @return The delay in milliseconds |
| */ |
| private long getDelayMillis() { |
| return Settings.Secure.getLong(mContentResolver, |
| LOCATION_ACCESS_CHECK_DELAY_MILLIS, MINUTES.toMillis(10)); |
| } |
| |
| /** |
| * Minimum time in between showing two notifications. |
| * |
| * <p>This is just small enough so that the periodic check can always show a notification. |
| * |
| * @return The minimum time in milliseconds |
| */ |
| private long getInBetweenNotificationsMillis() { |
| return getPeriodicCheckIntervalMillis() - (long) (getFlexForPeriodicCheckMillis() * 2.1); |
| } |
| |
| /** |
| * Load the list of {@link UserPackage packages} we already shown a notification for. |
| * |
| * @return The list of packages we already shown a notification for. |
| */ |
| private @NonNull ArraySet<UserPackage> loadAlreadyNotifiedPackagesLocked() { |
| try (BufferedReader reader = new BufferedReader(new InputStreamReader( |
| mContext.openFileInput(LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE)))) { |
| ArraySet<UserPackage> packages = new ArraySet<>(); |
| |
| /* |
| * The format of the file is <package> <serial of user>, e.g. |
| * |
| * com.one.package 5630633845 |
| * com.two.package 5630633853 |
| * com.three.package 5630633853 |
| */ |
| while (true) { |
| String line = reader.readLine(); |
| if (line == null) { |
| break; |
| } |
| |
| String[] lineComponents = line.split(" "); |
| String pkg = lineComponents[0]; |
| UserHandle user = mUserManager.getUserForSerialNumber( |
| Long.valueOf(lineComponents[1])); |
| |
| if (user != null) { |
| packages.add(new UserPackage(mContext, pkg, user)); |
| } else { |
| Log.i(LOG_TAG, "Not restoring state \"" + line + "\" as user is unknown"); |
| } |
| } |
| |
| return packages; |
| } catch (FileNotFoundException ignored) { |
| return new ArraySet<>(); |
| } catch (Exception e) { |
| Log.w(LOG_TAG, "Could not read " + LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE, e); |
| return new ArraySet<>(); |
| } |
| } |
| |
| /** |
| * Safe the list of {@link UserPackage packages} we have already shown a notification for. |
| * |
| * @param packages The list of packages we already shown a notification for. |
| */ |
| private void safeAlreadyNotifiedPackagesLocked(@NonNull ArraySet<UserPackage> packages) { |
| try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( |
| mContext.openFileOutput(LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE, |
| MODE_PRIVATE)))) { |
| /* |
| * The format of the file is <package> <serial of user>, e.g. |
| * |
| * com.one.package 5630633845 |
| * com.two.package 5630633853 |
| * com.three.package 5630633853 |
| */ |
| int numPkgs = packages.size(); |
| for (int i = 0; i < numPkgs; i++) { |
| UserPackage userPkg = packages.valueAt(i); |
| |
| writer.append(userPkg.pkg); |
| writer.append(' '); |
| writer.append( |
| Long.valueOf(mUserManager.getSerialNumberForUser(userPkg.user)).toString()); |
| writer.newLine(); |
| } |
| } catch (IOException e) { |
| Log.e(LOG_TAG, "Could not write " + LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE, e); |
| } |
| } |
| |
| /** |
| * Remember that we showed a notification for a {@link UserPackage} |
| * |
| * @param pkg The package we notified for |
| * @param user The user we notified for |
| */ |
| private void markAsNotified(@NonNull String pkg, @NonNull UserHandle user) { |
| synchronized (sLock) { |
| ArraySet<UserPackage> alreadyNotifiedPackages = loadAlreadyNotifiedPackagesLocked(); |
| alreadyNotifiedPackages.add(new UserPackage(mContext, pkg, user)); |
| safeAlreadyNotifiedPackagesLocked(alreadyNotifiedPackages); |
| } |
| } |
| |
| /** |
| * Create the channel the location access notifications should be posted to. |
| * |
| * @param user The user to create the channel for |
| */ |
| private void createPermissionReminderChannel(@NonNull UserHandle user) { |
| NotificationManager notificationManager = getSystemServiceSafe(mContext, |
| NotificationManager.class, user); |
| |
| NotificationChannel permissionReminderChannel = new NotificationChannel( |
| PERMISSION_REMINDER_CHANNEL_ID, mContext.getString(R.string.permission_reminders), |
| IMPORTANCE_HIGH); |
| notificationManager.createNotificationChannel(permissionReminderChannel); |
| } |
| |
| /** |
| * If {@link #mShouldCancel} throw an {@link InterruptedException}. |
| */ |
| private void throwInterruptedExceptionIfTaskIsCanceled() throws InterruptedException { |
| if (mShouldCancel != null && mShouldCancel.getAsBoolean()) { |
| throw new InterruptedException(); |
| } |
| } |
| |
| /** |
| * Create a new {@link LocationAccessCheck} object. |
| * |
| * @param context Used to resolve managers |
| * @param shouldCancel If supplied, can be used to interrupt long running operations |
| */ |
| public LocationAccessCheck(@NonNull Context context, @Nullable BooleanSupplier shouldCancel) { |
| UserHandle parentUser = getSystemServiceSafe(context, UserManager.class) |
| .getProfileParent(UserHandle.of(myUserId())); |
| |
| if (parentUser != null) { |
| // In a multi profile environment perform all operations as the parent user of the |
| // current profile |
| try { |
| mContext = context.createPackageContextAsUser(context.getPackageName(), 0, |
| parentUser); |
| } catch (PackageManager.NameNotFoundException e) { |
| // cannot happen |
| throw new IllegalStateException("Could not switch to parent user " + parentUser, e); |
| } |
| } else { |
| mContext = context; |
| } |
| |
| mJobScheduler = getSystemServiceSafe(mContext, JobScheduler.class); |
| mAppOpsManager = getSystemServiceSafe(mContext, AppOpsManager.class); |
| mPackageManager = mContext.getPackageManager(); |
| mUserManager = getSystemServiceSafe(mContext, UserManager.class); |
| mSharedPrefs = mContext.getSharedPreferences(PREFERENCES_FILE, MODE_PRIVATE); |
| mContentResolver = mContext.getContentResolver(); |
| |
| mShouldCancel = shouldCancel; |
| } |
| |
| /** |
| * Check if a location access notification should be shown and then add it. |
| * |
| * <p>Always run async inside a |
| * {@link LocationAccessCheckJobService.AddLocationNotificationIfNeededTask}. |
| */ |
| @WorkerThread |
| private void addLocationNotificationIfNeeded(@NonNull JobParameters params, |
| @NonNull LocationAccessCheckJobService service) { |
| if (!isLocationAccessCheckEnabled()) { |
| return; |
| } |
| |
| synchronized (sLock) { |
| try { |
| if (currentTimeMillis() - mSharedPrefs.getLong( |
| KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN, 0) |
| < getInBetweenNotificationsMillis()) { |
| service.jobFinished(params, false); |
| return; |
| } |
| |
| if (getCurrentlyShownNotificationLocked() != null) { |
| service.jobFinished(params, false); |
| return; |
| } |
| |
| HistoricalOpsRequest request = new HistoricalOpsRequest.Builder( |
| Instant.EPOCH.toEpochMilli(), Long.MAX_VALUE) |
| .setOpNames(CollectionUtils.singletonOrEmpty(OPSTR_FINE_LOCATION)) |
| .build(); |
| HistoricalOps[] ops = new HistoricalOps[1]; |
| mAppOpsManager.getHistoricalOps(request, |
| mContext.getMainExecutor(), (h) -> { |
| synchronized (ops) { |
| ops[0] = h; |
| ops.notifyAll(); |
| } |
| } |
| ); |
| |
| synchronized (ops) { |
| while (ops[0] == null) { |
| ops.wait(); |
| } |
| } |
| |
| addLocationNotificationIfNeeded(ops[0]); |
| service.jobFinished(params, false); |
| } catch (InterruptedException e) { |
| service.jobFinished(params, true); |
| } finally { |
| synchronized (sLock) { |
| service.mAddLocationNotificationIfNeededTask = null; |
| } |
| } |
| } |
| } |
| |
| private void addLocationNotificationIfNeeded(@NonNull HistoricalOps ops) |
| throws InterruptedException { |
| synchronized (sLock) { |
| List<UserPackage> packages = getLocationUsersWithNoNotificationYetLocked(ops); |
| |
| // Get a random package and resolve package info |
| PackageInfo pkgInfo = null; |
| while (pkgInfo == null) { |
| throwInterruptedExceptionIfTaskIsCanceled(); |
| |
| if (packages.isEmpty()) { |
| return; |
| } |
| |
| UserPackage packageToNotifyFor = null; |
| |
| // Prefer to show notification for location controller extra package |
| int numPkgs = packages.size(); |
| for (int i = 0; i < numPkgs; i++) { |
| UserPackage pkg = packages.get(i); |
| |
| LocationManager locationManager = getSystemServiceSafe(mContext, |
| LocationManager.class, pkg.user); |
| if (locationManager.isLocationControllerExtraPackageEnabled() && pkg.pkg.equals( |
| locationManager.getLocationControllerExtraPackage())) { |
| packageToNotifyFor = pkg; |
| break; |
| } |
| } |
| |
| if (packageToNotifyFor == null) { |
| packageToNotifyFor = packages.get(mRandom.nextInt(packages.size())); |
| } |
| |
| try { |
| pkgInfo = packageToNotifyFor.getPackageInfo(); |
| } catch (PackageManager.NameNotFoundException e) { |
| packages.remove(packageToNotifyFor); |
| } |
| } |
| |
| createPermissionReminderChannel(getUserHandleForUid(pkgInfo.applicationInfo.uid)); |
| createNotificationForLocationUser(pkgInfo); |
| } |
| } |
| |
| /** |
| * Get the {@link UserPackage packages} which accessed the location but we have not yet shown |
| * a notification for. |
| * |
| * <p>This also ignores all packages that are excepted from the notification. |
| * |
| * @return The packages we need to show a notification for |
| * |
| * @throws InterruptedException If {@link #mShouldCancel} |
| */ |
| private @NonNull List<UserPackage> getLocationUsersWithNoNotificationYetLocked( |
| @NonNull HistoricalOps allOps) throws InterruptedException { |
| List<UserPackage> pkgsWithLocationAccess = new ArrayList<>(); |
| List<UserHandle> profiles = mUserManager.getUserProfiles(); |
| |
| LocationManager lm = mContext.getSystemService(LocationManager.class); |
| |
| int numUid = allOps.getUidCount(); |
| for (int uidNum = 0; uidNum < numUid; uidNum++) { |
| AppOpsManager.HistoricalUidOps uidOps = allOps.getUidOpsAt(uidNum); |
| |
| int numPkgs = uidOps.getPackageCount(); |
| for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) { |
| HistoricalPackageOps ops = uidOps.getPackageOpsAt(pkgNum); |
| |
| String pkg = ops.getPackageName(); |
| if (pkg.equals(OS_PKG) || lm.isProviderPackage(pkg)) { |
| continue; |
| } |
| |
| UserHandle user = getUserHandleForUid(uidOps.getUid()); |
| // Do not handle apps that belong to a different profile user group |
| if (!profiles.contains(user)) { |
| continue; |
| } |
| |
| UserPackage userPkg = new UserPackage(mContext, pkg, user); |
| |
| AppPermissionGroup bgLocationGroup; |
| try { |
| bgLocationGroup = userPkg.getBackgroundLocationGroup(); |
| } catch (PackageManager.NameNotFoundException e) { |
| // Package was uninstalled |
| continue; |
| } |
| |
| // Do not show notification that do not request the background permission anymore |
| if (bgLocationGroup == null) { |
| continue; |
| } |
| |
| // Do not show notification that do not currently have the background permission |
| // granted |
| if (!bgLocationGroup.areRuntimePermissionsGranted()) { |
| continue; |
| } |
| |
| // Do not show notification for apps that have the background permission by default |
| if (bgLocationGroup.hasGrantedByDefaultPermission()) { |
| continue; |
| } |
| |
| int numOps = ops.getOpCount(); |
| for (int opNum = 0; opNum < numOps; opNum++) { |
| AppOpsManager.HistoricalOp op = ops.getOpAt(opNum); |
| |
| if (op.getBackgroundAccessCount() > 0) { |
| pkgsWithLocationAccess.add(userPkg); |
| |
| break; |
| } |
| } |
| } |
| } |
| |
| ArraySet<UserPackage> alreadyNotifiedPkgs = loadAlreadyNotifiedPackagesLocked(); |
| throwInterruptedExceptionIfTaskIsCanceled(); |
| |
| resetAlreadyNotifiedPackagesWithoutPermissionLocked(alreadyNotifiedPkgs); |
| |
| pkgsWithLocationAccess.removeAll(alreadyNotifiedPkgs); |
| return pkgsWithLocationAccess; |
| } |
| |
| /** |
| * Create a notification reminding the user that a package used the location. From this |
| * notification the user can directly go to the screen that allows to change the permission. |
| * |
| * @param pkg The {@link PackageInfo} for the package to to be changed |
| */ |
| private void createNotificationForLocationUser(@NonNull PackageInfo pkg) { |
| CharSequence pkgLabel = mPackageManager.getApplicationLabel(pkg.applicationInfo); |
| Drawable pkgIcon = mPackageManager.getApplicationIcon(pkg.applicationInfo); |
| Bitmap pkgIconBmp = createBitmap(pkgIcon.getIntrinsicWidth(), pkgIcon.getIntrinsicHeight(), |
| ARGB_8888); |
| Canvas canvas = new Canvas(pkgIconBmp); |
| pkgIcon.setBounds(0, 0, pkgIcon.getIntrinsicWidth(), pkgIcon.getIntrinsicHeight()); |
| pkgIcon.draw(canvas); |
| |
| String pkgName = pkg.packageName; |
| UserHandle user = getUserHandleForUid(pkg.applicationInfo.uid); |
| |
| NotificationManager notificationManager = getSystemServiceSafe(mContext, |
| NotificationManager.class, user); |
| |
| Intent deleteIntent = new Intent(mContext, NotificationDeleteHandler.class); |
| deleteIntent.putExtra(EXTRA_PACKAGE_NAME, pkgName); |
| deleteIntent.putExtra(EXTRA_USER, user); |
| |
| Intent clickIntent = new Intent(mContext, NotificationClickHandler.class); |
| clickIntent.putExtra(EXTRA_PACKAGE_NAME, pkgName); |
| clickIntent.putExtra(EXTRA_USER, user); |
| |
| Notification.Builder b = (new Notification.Builder(mContext, |
| PERMISSION_REMINDER_CHANNEL_ID)) |
| .setContentTitle(mContext.getString( |
| R.string.background_location_access_reminder_notification_title, pkgLabel)) |
| .setContentText(mContext.getString( |
| R.string.background_location_access_reminder_notification_content)) |
| .setStyle(new Notification.BigTextStyle().bigText(mContext.getString( |
| R.string.background_location_access_reminder_notification_content))) |
| .setSmallIcon(R.drawable.ic_signal_location) |
| .setLargeIcon(pkgIconBmp) |
| .setColor(mContext.getColor(android.R.color.system_notification_accent_color)) |
| .setAutoCancel(true) |
| .setDeleteIntent(getBroadcast(mContext, 0, deleteIntent, |
| FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT)) |
| .setContentIntent(getBroadcast(mContext, 0, clickIntent, |
| FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT)); |
| notificationManager.notify(pkgName, LOCATION_ACCESS_CHECK_NOTIFICATION_ID, b.build()); |
| |
| if (DEBUG) Log.i(LOG_TAG, "Notified " + pkgName); |
| |
| mSharedPrefs.edit().putLong(KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN, |
| currentTimeMillis()).apply(); |
| } |
| |
| /** |
| * Get currently shown notification. We only ever show one notification per profile group. |
| * |
| * @return The notification or {@code null} if no notification is currently shown |
| */ |
| private @Nullable StatusBarNotification getCurrentlyShownNotificationLocked() { |
| List<UserHandle> profiles = mUserManager.getUserProfiles(); |
| |
| int numProfiles = profiles.size(); |
| for (int profileNum = 0; profileNum < numProfiles; profileNum++) { |
| NotificationManager notificationManager = getSystemServiceSafe(mContext, |
| NotificationManager.class, profiles.get(profileNum)); |
| |
| StatusBarNotification[] notifications = notificationManager.getActiveNotifications(); |
| |
| int numNotifications = notifications.length; |
| for (int notificationNum = 0; notificationNum < numNotifications; notificationNum++) { |
| StatusBarNotification notification = notifications[notificationNum]; |
| |
| if (notification.getId() == LOCATION_ACCESS_CHECK_NOTIFICATION_ID) { |
| return notification; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Go through the list of packages we already shown a notification for and remove those that do |
| * not request fine background location access. |
| * |
| * @param alreadyNotifiedPkgs The packages we already shown a notification for. This paramter is |
| * modified inside of this method. |
| * |
| * @throws InterruptedException If {@link #mShouldCancel} |
| */ |
| private void resetAlreadyNotifiedPackagesWithoutPermissionLocked( |
| @NonNull ArraySet<UserPackage> alreadyNotifiedPkgs) throws InterruptedException { |
| ArrayList<UserPackage> packagesToRemove = new ArrayList<>(); |
| |
| for (UserPackage userPkg : alreadyNotifiedPkgs) { |
| throwInterruptedExceptionIfTaskIsCanceled(); |
| |
| try { |
| AppPermissionGroup bgLocationGroup = userPkg.getBackgroundLocationGroup(); |
| if (bgLocationGroup == null || !bgLocationGroup.areRuntimePermissionsGranted()) { |
| packagesToRemove.add(userPkg); |
| } |
| } catch (PackageManager.NameNotFoundException e) { |
| packagesToRemove.add(userPkg); |
| } |
| } |
| |
| if (!packagesToRemove.isEmpty()) { |
| alreadyNotifiedPkgs.removeAll(packagesToRemove); |
| safeAlreadyNotifiedPackagesLocked(alreadyNotifiedPkgs); |
| throwInterruptedExceptionIfTaskIsCanceled(); |
| } |
| } |
| |
| /** |
| * Remove all persisted state for a package. |
| * |
| * @param pkg name of package |
| * @param user user the package belongs to |
| */ |
| private void forgetAboutPackage(@NonNull String pkg, @NonNull UserHandle user) { |
| synchronized (sLock) { |
| StatusBarNotification notification = getCurrentlyShownNotificationLocked(); |
| if (notification != null && notification.getUser().equals(user) |
| && notification.getTag().equals(pkg)) { |
| getSystemServiceSafe(mContext, NotificationManager.class, user).cancel( |
| pkg, LOCATION_ACCESS_CHECK_NOTIFICATION_ID); |
| } |
| |
| ArraySet<UserPackage> packages = loadAlreadyNotifiedPackagesLocked(); |
| packages.remove(new UserPackage(mContext, pkg, user)); |
| safeAlreadyNotifiedPackagesLocked(packages); |
| } |
| } |
| |
| /** |
| * After a small delay schedule a check if we should show a notification. |
| * |
| * <p>This is called when location access is granted to an app. In this case it is likely that |
| * the app will access the location soon. If this happens the notification will appear only a |
| * little after the user granted the location. |
| */ |
| public void checkLocationAccessSoon() { |
| JobInfo.Builder b = (new JobInfo.Builder(LOCATION_ACCESS_CHECK_JOB_ID, |
| new ComponentName(mContext, LocationAccessCheckJobService.class))) |
| .setMinimumLatency(getDelayMillis()); |
| |
| int scheduleResult = mJobScheduler.schedule(b.build()); |
| if (scheduleResult != RESULT_SUCCESS) { |
| Log.e(LOG_TAG, "Could not schedule location access check " + scheduleResult); |
| } |
| } |
| |
| /** |
| * Check if the current user is the profile parent. |
| * |
| * @return {@code true} if the current user is the profile parent. |
| */ |
| private boolean isRunningInParentProfile() { |
| UserHandle user = UserHandle.of(myUserId()); |
| UserHandle parent = mUserManager.getProfileParent(user); |
| |
| return parent == null || user.equals(parent); |
| } |
| |
| /** |
| * On boot set up a periodic job that starts checks. |
| */ |
| public static class SetupPeriodicBackgroundLocationAccessCheck extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| LocationAccessCheck locationAccessCheck = new LocationAccessCheck(context, null); |
| JobScheduler jobScheduler = getSystemServiceSafe(context, JobScheduler.class); |
| |
| if (!locationAccessCheck.isRunningInParentProfile()) { |
| // Profile parent handles child profiles too. |
| return; |
| } |
| |
| if (jobScheduler.getPendingJob(PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID) == null) { |
| JobInfo.Builder b = (new JobInfo.Builder(PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID, |
| new ComponentName(context, LocationAccessCheckJobService.class))) |
| .setPeriodic(locationAccessCheck.getPeriodicCheckIntervalMillis(), |
| locationAccessCheck.getFlexForPeriodicCheckMillis()); |
| |
| int scheduleResult = jobScheduler.schedule(b.build()); |
| if (scheduleResult != RESULT_SUCCESS) { |
| Log.e(LOG_TAG, "Could not schedule periodic location access check " |
| + scheduleResult); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Checks if a new notification should be shown. |
| */ |
| public static class LocationAccessCheckJobService extends JobService { |
| private LocationAccessCheck mLocationAccessCheck; |
| |
| /** If we currently check if we should show a notification, the task executing the check */ |
| // @GuardedBy("sLock") |
| private @Nullable AddLocationNotificationIfNeededTask mAddLocationNotificationIfNeededTask; |
| |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| mLocationAccessCheck = new LocationAccessCheck(this, () -> { |
| synchronized (sLock) { |
| AddLocationNotificationIfNeededTask task = mAddLocationNotificationIfNeededTask; |
| |
| return task != null && task.isCancelled(); |
| } |
| }); |
| } |
| |
| /** |
| * Starts an asynchronous check if a location access notification should be shown. |
| * |
| * @param params Not used other than for interacting with job scheduling |
| * |
| * @return {@code false} iff another check if already running |
| */ |
| @Override |
| public boolean onStartJob(JobParameters params) { |
| synchronized (LocationAccessCheck.sLock) { |
| if (mAddLocationNotificationIfNeededTask != null) { |
| return false; |
| } |
| |
| mAddLocationNotificationIfNeededTask = |
| new AddLocationNotificationIfNeededTask(); |
| |
| mAddLocationNotificationIfNeededTask.execute(params, this); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Abort the check if still running. |
| * |
| * @param params ignored |
| * |
| * @return false |
| */ |
| @Override |
| public boolean onStopJob(JobParameters params) { |
| AddLocationNotificationIfNeededTask task; |
| synchronized (sLock) { |
| if (mAddLocationNotificationIfNeededTask == null) { |
| return false; |
| } else { |
| task = mAddLocationNotificationIfNeededTask; |
| } |
| } |
| |
| task.cancel(false); |
| |
| try { |
| // Wait for task to finish |
| task.get(); |
| } catch (Exception e) { |
| Log.e(LOG_TAG, "While waiting for " + task + " to finish", e); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * A {@link AsyncTask task} that runs the check in the background. |
| */ |
| private class AddLocationNotificationIfNeededTask extends |
| AsyncTask<Object, Void, Void> { |
| @Override |
| protected final Void doInBackground(Object... in) { |
| JobParameters params = (JobParameters) in[0]; |
| LocationAccessCheckJobService service = (LocationAccessCheckJobService) in[1]; |
| mLocationAccessCheck.addLocationNotificationIfNeeded(params, service); |
| return null; |
| } |
| } |
| } |
| |
| /** |
| * Handle the case where the notification is swiped away without further interaction. |
| */ |
| public static class NotificationDeleteHandler extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String pkg = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME); |
| UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER); |
| |
| new LocationAccessCheck(context, null).markAsNotified(pkg, user); |
| } |
| } |
| |
| /** |
| * Show the location permission switch when the notification is clicked. |
| */ |
| public static class NotificationClickHandler extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String pkg = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME); |
| UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER); |
| |
| new LocationAccessCheck(context, null).markAsNotified(pkg, user); |
| |
| Intent manageAppPermission = new Intent(context, AppPermissionActivity.class); |
| manageAppPermission.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK); |
| manageAppPermission.putExtra(EXTRA_PERMISSION_NAME, |
| getGroupOfPlatformPermission(ACCESS_FINE_LOCATION)); |
| manageAppPermission.putExtra(EXTRA_PACKAGE_NAME, pkg); |
| manageAppPermission.putExtra(EXTRA_USER, user); |
| |
| context.startActivity(manageAppPermission); |
| } |
| } |
| |
| /** |
| * If a package gets removed or the data of the package gets cleared, forget that we showed a |
| * notification for it. |
| */ |
| public static class PackageResetHandler extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| Uri data = Preconditions.checkNotNull(intent.getData()); |
| UserHandle user = getUserHandleForUid(intent.getIntExtra(EXTRA_UID, 0)); |
| |
| if (DEBUG) Log.i(LOG_TAG, "Reset " + data.getSchemeSpecificPart()); |
| |
| new LocationAccessCheck(context, null).forgetAboutPackage( |
| data.getSchemeSpecificPart(), user); |
| } |
| } |
| |
| /** |
| * A immutable class containing a package name and a {@link UserHandle}. |
| */ |
| private static final class UserPackage { |
| private final @NonNull Context mContext; |
| |
| public final @NonNull String pkg; |
| public final @NonNull UserHandle user; |
| |
| /** |
| * Create a new {@link UserPackage} |
| * |
| * @param context A context to be used by methods of this object |
| * @param pkg The name of the package |
| * @param user The user the package belongs to |
| */ |
| UserPackage(@NonNull Context context, @NonNull String pkg, @NonNull UserHandle user) { |
| try { |
| mContext = context.createPackageContextAsUser(context.getPackageName(), 0, user); |
| } catch (PackageManager.NameNotFoundException e) { |
| throw new IllegalStateException(e); |
| } |
| |
| this.pkg = pkg; |
| this.user = user; |
| } |
| |
| /** |
| * Get {@link PackageInfo} for this user package. |
| * |
| * @return The package info |
| * |
| * @throws PackageManager.NameNotFoundException if package/user does not exist |
| */ |
| @NonNull PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException { |
| return mContext.getPackageManager().getPackageInfo(pkg, GET_PERMISSIONS); |
| } |
| |
| /** |
| * Get the {@link AppPermissionGroup} for |
| * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and this user package. |
| * |
| * @return The app permission group or {@code null} if the app does not request location |
| * |
| * @throws PackageManager.NameNotFoundException if package/user does not exist |
| */ |
| @Nullable AppPermissionGroup getLocationGroup() |
| throws PackageManager.NameNotFoundException { |
| return AppPermissionGroup.create(mContext, getPackageInfo(), ACCESS_FINE_LOCATION, |
| false); |
| } |
| |
| /** |
| * Get the {@link AppPermissionGroup} for the background location of |
| * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and this user package. |
| * |
| * @return The app permission group or {@code null} if the app does not request background |
| * location |
| * |
| * @throws PackageManager.NameNotFoundException if package/user does not exist |
| */ |
| @Nullable AppPermissionGroup getBackgroundLocationGroup() |
| throws PackageManager.NameNotFoundException { |
| AppPermissionGroup locationGroup = getLocationGroup(); |
| if (locationGroup == null) { |
| return null; |
| } |
| |
| return locationGroup.getBackgroundPermissions(); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (!(o instanceof UserPackage)) { |
| return false; |
| } |
| |
| UserPackage userPackage = (UserPackage) o; |
| return pkg.equals(userPackage.pkg) && user.equals(userPackage.user); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(pkg, user); |
| } |
| } |
| } |