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