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