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_ONE_SHOT; 24 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; 25 import static android.app.PendingIntent.getBroadcast; 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.EXTRA_PACKAGE_NAME; 30 import static android.content.Intent.EXTRA_PERMISSION_GROUP_NAME; 31 import static android.content.Intent.EXTRA_UID; 32 import static android.content.Intent.EXTRA_USER; 33 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; 34 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 35 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND; 36 import static android.content.pm.PackageManager.GET_PERMISSIONS; 37 import static android.graphics.Bitmap.Config.ARGB_8888; 38 import static android.graphics.Bitmap.createBitmap; 39 import static android.os.UserHandle.getUserHandleForUid; 40 import static android.os.UserHandle.myUserId; 41 import static android.provider.Settings.Secure.LOCATION_ACCESS_CHECK_DELAY_MILLIS; 42 import static android.provider.Settings.Secure.LOCATION_ACCESS_CHECK_INTERVAL_MILLIS; 43 44 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID; 45 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID; 46 import static com.android.permissioncontroller.Constants.KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN; 47 import static com.android.permissioncontroller.Constants.KEY_LOCATION_ACCESS_CHECK_ENABLED_TIME; 48 import static com.android.permissioncontroller.Constants.LOCATION_ACCESS_CHECK_ALREADY_NOTIFIED_FILE; 49 import static com.android.permissioncontroller.Constants.LOCATION_ACCESS_CHECK_JOB_ID; 50 import static com.android.permissioncontroller.Constants.LOCATION_ACCESS_CHECK_NOTIFICATION_ID; 51 import static com.android.permissioncontroller.Constants.PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID; 52 import static com.android.permissioncontroller.Constants.PERMISSION_REMINDER_CHANNEL_ID; 53 import static com.android.permissioncontroller.Constants.PREFERENCES_FILE; 54 import static com.android.permissioncontroller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION; 55 import static com.android.permissioncontroller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_CLICKED; 56 import static com.android.permissioncontroller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_DECLINED; 57 import static com.android.permissioncontroller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_PRESENTED; 58 import static com.android.permissioncontroller.permission.utils.Utils.OS_PKG; 59 import static com.android.permissioncontroller.permission.utils.Utils.getParcelableExtraSafe; 60 import static com.android.permissioncontroller.permission.utils.Utils.getParentUserContext; 61 import static com.android.permissioncontroller.permission.utils.Utils.getStringExtraSafe; 62 import static com.android.permissioncontroller.permission.utils.Utils.getSystemServiceSafe; 63 64 import static java.lang.System.currentTimeMillis; 65 import static java.util.concurrent.TimeUnit.DAYS; 66 67 import android.app.AppOpsManager; 68 import android.app.AppOpsManager.OpEntry; 69 import android.app.AppOpsManager.PackageOps; 70 import android.app.Notification; 71 import android.app.NotificationChannel; 72 import android.app.NotificationManager; 73 import android.app.job.JobInfo; 74 import android.app.job.JobParameters; 75 import android.app.job.JobScheduler; 76 import android.app.job.JobService; 77 import android.content.BroadcastReceiver; 78 import android.content.ComponentName; 79 import android.content.ContentResolver; 80 import android.content.Context; 81 import android.content.Intent; 82 import android.content.SharedPreferences; 83 import android.content.pm.PackageInfo; 84 import android.content.pm.PackageManager; 85 import android.graphics.Bitmap; 86 import android.graphics.Canvas; 87 import android.graphics.drawable.Drawable; 88 import android.location.LocationManager; 89 import android.net.Uri; 90 import android.os.AsyncTask; 91 import android.os.Bundle; 92 import android.os.UserHandle; 93 import android.os.UserManager; 94 import android.provider.Settings; 95 import android.service.notification.StatusBarNotification; 96 import android.util.ArraySet; 97 import android.util.Log; 98 99 import androidx.annotation.NonNull; 100 import androidx.annotation.Nullable; 101 import androidx.annotation.WorkerThread; 102 import androidx.core.util.Preconditions; 103 104 import com.android.permissioncontroller.PermissionControllerStatsLog; 105 import com.android.permissioncontroller.R; 106 import com.android.permissioncontroller.permission.model.AppPermissionGroup; 107 import com.android.permissioncontroller.permission.utils.Utils; 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 = Utils.getSettingsLabelForNotifications(mPackageManager); 591 592 Notification.Builder b = (new Notification.Builder(mContext, 593 PERMISSION_REMINDER_CHANNEL_ID)) 594 .setLocalOnly(true) 595 .setContentTitle(mContext.getString( 596 R.string.background_location_access_reminder_notification_title, pkgLabel)) 597 .setContentText(mContext.getString( 598 R.string.background_location_access_reminder_notification_content)) 599 .setStyle(new Notification.BigTextStyle().bigText(mContext.getString( 600 R.string.background_location_access_reminder_notification_content))) 601 .setSmallIcon(R.drawable.ic_pin_drop) 602 .setLargeIcon(pkgIconBmp) 603 .setColor(mContext.getColor(android.R.color.system_notification_accent_color)) 604 .setAutoCancel(true) 605 .setDeleteIntent(getBroadcast(mContext, 0, deleteIntent, 606 FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT)) 607 .setContentIntent(getBroadcast(mContext, 0, clickIntent, 608 FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT)); 609 610 if (appName != null) { 611 Bundle extras = new Bundle(); 612 extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, appName.toString()); 613 b.addExtras(extras); 614 } 615 616 notificationManager.notify(pkgName, LOCATION_ACCESS_CHECK_NOTIFICATION_ID, b.build()); 617 618 if (DEBUG) Log.i(LOG_TAG, "Notified " + pkgName); 619 620 PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId, 621 pkg.applicationInfo.uid, pkgName, 622 LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_PRESENTED); 623 Log.v(LOG_TAG, "Location access check notification shown with sessionId=" + sessionId + "" 624 + " uid=" + pkg.applicationInfo.uid + " pkgName=" + pkgName); 625 626 mSharedPrefs.edit().putLong(KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN, 627 currentTimeMillis()).apply(); 628 } 629 630 /** 631 * Get currently shown notification. We only ever show one notification per profile group. 632 * 633 * @return The notification or {@code null} if no notification is currently shown 634 */ getCurrentlyShownNotificationLocked()635 private @Nullable StatusBarNotification getCurrentlyShownNotificationLocked() { 636 List<UserHandle> profiles = mUserManager.getUserProfiles(); 637 638 int numProfiles = profiles.size(); 639 for (int profileNum = 0; profileNum < numProfiles; profileNum++) { 640 NotificationManager notificationManager; 641 try { 642 notificationManager = getSystemServiceSafe(mContext, NotificationManager.class, 643 profiles.get(profileNum)); 644 } catch (IllegalStateException e) { 645 continue; 646 } 647 648 StatusBarNotification[] notifications = notificationManager.getActiveNotifications(); 649 650 int numNotifications = notifications.length; 651 for (int notificationNum = 0; notificationNum < numNotifications; notificationNum++) { 652 StatusBarNotification notification = notifications[notificationNum]; 653 654 if (notification.getId() == LOCATION_ACCESS_CHECK_NOTIFICATION_ID) { 655 return notification; 656 } 657 } 658 } 659 660 return null; 661 } 662 663 /** 664 * Go through the list of packages we already shown a notification for and remove those that do 665 * not request fine background location access. 666 * 667 * @param alreadyNotifiedPkgs The packages we already shown a notification for. This paramter is 668 * modified inside of this method. 669 * 670 * @throws InterruptedException If {@link #mShouldCancel} 671 */ resetAlreadyNotifiedPackagesWithoutPermissionLocked( @onNull ArraySet<UserPackage> alreadyNotifiedPkgs)672 private void resetAlreadyNotifiedPackagesWithoutPermissionLocked( 673 @NonNull ArraySet<UserPackage> alreadyNotifiedPkgs) throws InterruptedException { 674 ArrayList<UserPackage> packagesToRemove = new ArrayList<>(); 675 676 for (UserPackage userPkg : alreadyNotifiedPkgs) { 677 throwInterruptedExceptionIfTaskIsCanceled(); 678 679 AppPermissionGroup bgLocationGroup = userPkg.getBackgroundLocationGroup(); 680 if (bgLocationGroup == null || !bgLocationGroup.areRuntimePermissionsGranted()) { 681 packagesToRemove.add(userPkg); 682 } 683 } 684 685 if (!packagesToRemove.isEmpty()) { 686 alreadyNotifiedPkgs.removeAll(packagesToRemove); 687 safeAlreadyNotifiedPackagesLocked(alreadyNotifiedPkgs); 688 throwInterruptedExceptionIfTaskIsCanceled(); 689 } 690 } 691 692 /** 693 * Remove all persisted state for a package. 694 * 695 * @param pkg name of package 696 * @param user user the package belongs to 697 */ forgetAboutPackage(@onNull String pkg, @NonNull UserHandle user)698 private void forgetAboutPackage(@NonNull String pkg, @NonNull UserHandle user) { 699 synchronized (sLock) { 700 StatusBarNotification notification = getCurrentlyShownNotificationLocked(); 701 if (notification != null && notification.getUser().equals(user) 702 && notification.getTag().equals(pkg)) { 703 getSystemServiceSafe(mContext, NotificationManager.class, user).cancel( 704 pkg, LOCATION_ACCESS_CHECK_NOTIFICATION_ID); 705 } 706 707 ArraySet<UserPackage> packages = loadAlreadyNotifiedPackagesLocked(); 708 packages.remove(new UserPackage(mContext, pkg, user)); 709 safeAlreadyNotifiedPackagesLocked(packages); 710 } 711 } 712 713 /** 714 * After a small delay schedule a check if we should show a notification. 715 * 716 * <p>This is called when location access is granted to an app. In this case it is likely that 717 * the app will access the location soon. If this happens the notification will appear only a 718 * little after the user granted the location. 719 */ checkLocationAccessSoon()720 public void checkLocationAccessSoon() { 721 JobInfo.Builder b = (new JobInfo.Builder(LOCATION_ACCESS_CHECK_JOB_ID, 722 new ComponentName(mContext, LocationAccessCheckJobService.class))) 723 .setMinimumLatency(getDelayMillis()); 724 725 int scheduleResult = mJobScheduler.schedule(b.build()); 726 if (scheduleResult != RESULT_SUCCESS) { 727 Log.e(LOG_TAG, "Could not schedule location access check " + scheduleResult); 728 } 729 } 730 731 /** 732 * Check if the current user is the profile parent. 733 * 734 * @return {@code true} if the current user is the profile parent. 735 */ isRunningInParentProfile()736 private boolean isRunningInParentProfile() { 737 UserHandle user = UserHandle.of(myUserId()); 738 UserHandle parent = mUserManager.getProfileParent(user); 739 740 return parent == null || user.equals(parent); 741 } 742 743 /** 744 * On boot set up a periodic job that starts checks. 745 */ 746 public static class SetupPeriodicBackgroundLocationAccessCheck extends BroadcastReceiver { 747 @Override onReceive(Context context, Intent intent)748 public void onReceive(Context context, Intent intent) { 749 LocationAccessCheck locationAccessCheck = new LocationAccessCheck(context, null); 750 JobScheduler jobScheduler = getSystemServiceSafe(context, JobScheduler.class); 751 752 if (!locationAccessCheck.isRunningInParentProfile()) { 753 // Profile parent handles child profiles too. 754 return; 755 } 756 757 // Init LocationAccessCheckEnabledTime if needed 758 locationAccessCheck.checkLocationAccessCheckEnabledAndUpdateEnabledTime(); 759 760 if (jobScheduler.getPendingJob(PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID) == null) { 761 JobInfo.Builder b = (new JobInfo.Builder(PERIODIC_LOCATION_ACCESS_CHECK_JOB_ID, 762 new ComponentName(context, LocationAccessCheckJobService.class))) 763 .setPeriodic(locationAccessCheck.getPeriodicCheckIntervalMillis(), 764 locationAccessCheck.getFlexForPeriodicCheckMillis()); 765 766 int scheduleResult = jobScheduler.schedule(b.build()); 767 if (scheduleResult != RESULT_SUCCESS) { 768 Log.e(LOG_TAG, "Could not schedule periodic location access check " 769 + scheduleResult); 770 } 771 } 772 } 773 } 774 775 /** 776 * Checks if a new notification should be shown. 777 */ 778 public static class LocationAccessCheckJobService extends JobService { 779 private LocationAccessCheck mLocationAccessCheck; 780 781 /** If we currently check if we should show a notification, the task executing the check */ 782 // @GuardedBy("sLock") 783 private @Nullable AddLocationNotificationIfNeededTask mAddLocationNotificationIfNeededTask; 784 785 @Override onCreate()786 public void onCreate() { 787 super.onCreate(); 788 mLocationAccessCheck = new LocationAccessCheck(this, () -> { 789 synchronized (sLock) { 790 AddLocationNotificationIfNeededTask task = mAddLocationNotificationIfNeededTask; 791 792 return task != null && task.isCancelled(); 793 } 794 }); 795 } 796 797 /** 798 * Starts an asynchronous check if a location access notification should be shown. 799 * 800 * @param params Not used other than for interacting with job scheduling 801 * 802 * @return {@code false} iff another check if already running 803 */ 804 @Override onStartJob(JobParameters params)805 public boolean onStartJob(JobParameters params) { 806 synchronized (LocationAccessCheck.sLock) { 807 if (mAddLocationNotificationIfNeededTask != null) { 808 return false; 809 } 810 811 mAddLocationNotificationIfNeededTask = 812 new AddLocationNotificationIfNeededTask(); 813 814 mAddLocationNotificationIfNeededTask.execute(params, this); 815 } 816 817 return true; 818 } 819 820 /** 821 * Abort the check if still running. 822 * 823 * @param params ignored 824 * 825 * @return false 826 */ 827 @Override onStopJob(JobParameters params)828 public boolean onStopJob(JobParameters params) { 829 AddLocationNotificationIfNeededTask task; 830 synchronized (sLock) { 831 if (mAddLocationNotificationIfNeededTask == null) { 832 return false; 833 } else { 834 task = mAddLocationNotificationIfNeededTask; 835 } 836 } 837 838 task.cancel(false); 839 840 try { 841 // Wait for task to finish 842 task.get(); 843 } catch (Exception e) { 844 Log.e(LOG_TAG, "While waiting for " + task + " to finish", e); 845 } 846 847 return false; 848 } 849 850 /** 851 * A {@link AsyncTask task} that runs the check in the background. 852 */ 853 private class AddLocationNotificationIfNeededTask extends 854 AsyncTask<Object, Void, Void> { 855 @Override doInBackground(Object... in)856 protected final Void doInBackground(Object... in) { 857 JobParameters params = (JobParameters) in[0]; 858 LocationAccessCheckJobService service = (LocationAccessCheckJobService) in[1]; 859 mLocationAccessCheck.addLocationNotificationIfNeeded(params, service); 860 return null; 861 } 862 } 863 } 864 865 /** 866 * Handle the case where the notification is swiped away without further interaction. 867 */ 868 public static class NotificationDeleteHandler extends BroadcastReceiver { 869 @Override onReceive(Context context, Intent intent)870 public void onReceive(Context context, Intent intent) { 871 String pkg = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME); 872 UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER); 873 long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID); 874 int uid = intent.getIntExtra(EXTRA_UID, 0); 875 876 PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId, 877 uid, pkg, 878 LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_DECLINED); 879 Log.v(LOG_TAG, 880 "Location access check notification declined with sessionId=" + sessionId + "" 881 + " uid=" + uid + " pkgName=" + pkg); 882 883 new LocationAccessCheck(context, null).markAsNotified(pkg, user); 884 } 885 } 886 887 /** 888 * Show the location permission switch when the notification is clicked. 889 */ 890 public static class NotificationClickHandler extends BroadcastReceiver { 891 @Override onReceive(Context context, Intent intent)892 public void onReceive(Context context, Intent intent) { 893 String pkg = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME); 894 UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER); 895 int uid = intent.getIntExtra(EXTRA_UID, 0); 896 long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID); 897 898 new LocationAccessCheck(context, null).markAsNotified(pkg, user); 899 900 PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId, 901 uid, pkg, 902 LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_CLICKED); 903 Log.v(LOG_TAG, 904 "Location access check notification clicked with sessionId=" + sessionId + "" 905 + " uid=" + uid + " pkgName=" + pkg); 906 907 Intent manageAppPermission = new Intent(ACTION_MANAGE_APP_PERMISSION); 908 manageAppPermission.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK); 909 manageAppPermission.putExtra(EXTRA_PERMISSION_GROUP_NAME, LOCATION); 910 manageAppPermission.putExtra(EXTRA_PACKAGE_NAME, pkg); 911 manageAppPermission.putExtra(EXTRA_USER, user); 912 manageAppPermission.putExtra(EXTRA_SESSION_ID, sessionId); 913 914 915 context.startActivity(manageAppPermission); 916 } 917 } 918 919 /** 920 * If a package gets removed or the data of the package gets cleared, forget that we showed a 921 * notification for it. 922 */ 923 public static class PackageResetHandler extends BroadcastReceiver { 924 @Override onReceive(Context context, Intent intent)925 public void onReceive(Context context, Intent intent) { 926 String action = intent.getAction(); 927 if (!(Objects.equals(action, Intent.ACTION_PACKAGE_DATA_CLEARED) 928 || Objects.equals(action, Intent.ACTION_PACKAGE_FULLY_REMOVED))) { 929 return; 930 } 931 932 Uri data = Preconditions.checkNotNull(intent.getData()); 933 UserHandle user = getUserHandleForUid(intent.getIntExtra(EXTRA_UID, 0)); 934 935 if (DEBUG) Log.i(LOG_TAG, "Reset " + data.getSchemeSpecificPart()); 936 937 new LocationAccessCheck(context, null).forgetAboutPackage( 938 data.getSchemeSpecificPart(), user); 939 } 940 } 941 942 /** 943 * A immutable class containing a package name and a {@link UserHandle}. 944 */ 945 private static final class UserPackage { 946 private final @NonNull Context mContext; 947 948 public final @NonNull String pkg; 949 public final @NonNull UserHandle user; 950 951 /** 952 * Create a new {@link UserPackage} 953 * 954 * @param context A context to be used by methods of this object 955 * @param pkg The name of the package 956 * @param user The user the package belongs to 957 */ UserPackage(@onNull Context context, @NonNull String pkg, @NonNull UserHandle user)958 UserPackage(@NonNull Context context, @NonNull String pkg, @NonNull UserHandle user) { 959 try { 960 mContext = context.createPackageContextAsUser(context.getPackageName(), 0, user); 961 } catch (PackageManager.NameNotFoundException e) { 962 throw new IllegalStateException(e); 963 } 964 965 this.pkg = pkg; 966 this.user = user; 967 } 968 969 /** 970 * Get {@link PackageInfo} for this user package. 971 * 972 * @return The package info 973 * 974 * @throws PackageManager.NameNotFoundException if package/user does not exist 975 */ getPackageInfo()976 @NonNull PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException { 977 return mContext.getPackageManager().getPackageInfo(pkg, GET_PERMISSIONS); 978 } 979 980 /** 981 * Get the {@link AppPermissionGroup} for 982 * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and this user package. 983 * 984 * @return The app permission group or {@code null} if the app does not request location 985 */ getLocationGroup()986 @Nullable AppPermissionGroup getLocationGroup() { 987 try { 988 return AppPermissionGroup.create(mContext, getPackageInfo(), ACCESS_FINE_LOCATION, 989 false); 990 } catch (PackageManager.NameNotFoundException e) { 991 return null; 992 } 993 } 994 995 /** 996 * Get the {@link AppPermissionGroup} for the background location of 997 * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and this user package. 998 * 999 * @return The app permission group or {@code null} if the app does not request background 1000 * location 1001 */ getBackgroundLocationGroup()1002 @Nullable AppPermissionGroup getBackgroundLocationGroup() { 1003 AppPermissionGroup locationGroup = getLocationGroup(); 1004 if (locationGroup == null) { 1005 return null; 1006 } 1007 1008 return locationGroup.getBackgroundPermissions(); 1009 } 1010 1011 @Override equals(Object o)1012 public boolean equals(Object o) { 1013 if (!(o instanceof UserPackage)) { 1014 return false; 1015 } 1016 1017 UserPackage userPackage = (UserPackage) o; 1018 return pkg.equals(userPackage.pkg) && user.equals(userPackage.user); 1019 } 1020 1021 @Override hashCode()1022 public int hashCode() { 1023 return Objects.hash(pkg, user); 1024 } 1025 } 1026 } 1027