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