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