1 /* <lambda>null2 * Copyright (C) 2023 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.v34 18 19 import android.Manifest 20 import android.app.Notification 21 import android.app.NotificationChannel 22 import android.app.NotificationManager 23 import android.app.PendingIntent 24 import android.app.job.JobInfo 25 import android.app.job.JobParameters 26 import android.app.job.JobScheduler 27 import android.app.job.JobService 28 import android.content.BroadcastReceiver 29 import android.content.ComponentName 30 import android.content.Context 31 import android.content.Intent 32 import android.content.Intent.ACTION_BOOT_COMPLETED 33 import android.content.pm.PackageManager 34 import android.os.Build 35 import android.os.Bundle 36 import android.os.PersistableBundle 37 import android.os.Process 38 import android.os.UserHandle 39 import android.os.UserManager 40 import android.provider.DeviceConfig 41 import android.util.Log 42 import androidx.annotation.RequiresApi 43 import androidx.core.app.NotificationCompat 44 import androidx.core.graphics.drawable.IconCompat 45 import com.android.permission.safetylabel.DataCategoryConstants.CATEGORY_LOCATION 46 import com.android.permission.safetylabel.SafetyLabel as AppMetadataSafetyLabel 47 import com.android.permissioncontroller.Constants.EXTRA_SESSION_ID 48 import com.android.permissioncontroller.Constants.INVALID_SESSION_ID 49 import com.android.permissioncontroller.Constants.PERMISSION_REMINDER_CHANNEL_ID 50 import com.android.permissioncontroller.Constants.SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID 51 import com.android.permissioncontroller.Constants.SAFETY_LABEL_CHANGES_NOTIFICATION_ID 52 import com.android.permissioncontroller.Constants.SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID 53 import com.android.permissioncontroller.PermissionControllerApplication 54 import com.android.permissioncontroller.PermissionControllerStatsLog 55 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION 56 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION__ACTION__DISMISSED 57 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_SHOWN 58 import com.android.permissioncontroller.R 59 import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData 60 import com.android.permissioncontroller.permission.data.SinglePermGroupPackagesUiInfoLiveData 61 import com.android.permissioncontroller.permission.data.get 62 import com.android.permissioncontroller.permission.data.v34.AppDataSharingUpdatesLiveData 63 import com.android.permissioncontroller.permission.data.v34.LightInstallSourceInfoLiveData 64 import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo 65 import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo.PermGrantState.PERMS_ALLOWED_ALWAYS 66 import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo.PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY 67 import com.android.permissioncontroller.permission.model.v34.AppDataSharingUpdate 68 import com.android.permissioncontroller.permission.utils.KotlinUtils 69 import com.android.permissioncontroller.permission.utils.Utils.getSystemServiceSafe 70 import com.android.permissioncontroller.permission.utils.v34.SafetyLabelUtils 71 import com.android.permissioncontroller.safetylabel.AppsSafetyLabelHistory 72 import com.android.permissioncontroller.safetylabel.AppsSafetyLabelHistory.AppInfo 73 import com.android.permissioncontroller.safetylabel.AppsSafetyLabelHistory.SafetyLabel as SafetyLabelForPersistence 74 import com.android.permissioncontroller.safetylabel.AppsSafetyLabelHistoryPersistence 75 import com.android.permissioncontroller.safetylabel.SafetyLabelChangedBroadcastReceiver 76 import java.time.Duration 77 import java.time.Instant 78 import java.time.ZoneId 79 import java.util.Random 80 import kotlinx.coroutines.Dispatchers 81 import kotlinx.coroutines.GlobalScope 82 import kotlinx.coroutines.Job 83 import kotlinx.coroutines.launch 84 import kotlinx.coroutines.runBlocking 85 import kotlinx.coroutines.sync.Mutex 86 import kotlinx.coroutines.sync.withLock 87 import kotlinx.coroutines.yield 88 89 /** 90 * Runs a monthly job that performs Safety Labels-related tasks. (E.g., data policy changes 91 * notification, hygiene, etc.) 92 */ 93 // TODO(b/265202443): Review support for safe cancellation of this Job. Currently this is 94 // implemented by implementing `onStopJob` method and including `yield()` calls in computation 95 // loops. 96 // TODO(b/276511043): Refactor this class into separate components 97 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 98 class SafetyLabelChangesJobService : JobService() { 99 private val mutex = Mutex() 100 private var detectUpdatesJob: Job? = null 101 private var notificationJob: Job? = null 102 private val context = this@SafetyLabelChangesJobService 103 private val random = Random() 104 105 class Receiver : BroadcastReceiver() { 106 override fun onReceive(receiverContext: Context, intent: Intent) { 107 if (DEBUG) { 108 Log.d(LOG_TAG, "Received broadcast with intent action '${intent.action}'") 109 } 110 if (!KotlinUtils.isSafetyLabelChangeNotificationsEnabled(receiverContext)) { 111 Log.i(LOG_TAG, "onReceive: Safety label change notifications are not enabled.") 112 return 113 } 114 if (KotlinUtils.safetyLabelChangesJobServiceKillSwitch()) { 115 Log.i(LOG_TAG, "onReceive: kill switch is set.") 116 return 117 } 118 if (isContextInProfileUser(receiverContext)) { 119 Log.i( 120 LOG_TAG, 121 "onReceive: Received broadcast in profile, not scheduling safety label" + 122 " change job" 123 ) 124 return 125 } 126 if ( 127 intent.action != ACTION_BOOT_COMPLETED && 128 intent.action != ACTION_SET_UP_SAFETY_LABEL_CHANGES_JOB 129 ) { 130 return 131 } 132 scheduleDetectUpdatesJob(receiverContext) 133 schedulePeriodicNotificationJob(receiverContext) 134 } 135 136 private fun isContextInProfileUser(context: Context): Boolean { 137 val userManager: UserManager = context.getSystemService(UserManager::class.java)!! 138 return userManager.isProfile 139 } 140 } 141 142 /** Handle the case where the notification is swiped away without further interaction. */ 143 class NotificationDeleteHandler : BroadcastReceiver() { 144 override fun onReceive(receiverContext: Context, intent: Intent) { 145 Log.d(LOG_TAG, "NotificationDeleteHandler: received broadcast") 146 if (!KotlinUtils.isSafetyLabelChangeNotificationsEnabled(receiverContext)) { 147 Log.i( 148 LOG_TAG, 149 "NotificationDeleteHandler: " + 150 "safety label change notifications are not enabled." 151 ) 152 return 153 } 154 val sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID) 155 val numberOfAppUpdates = intent.getIntExtra(EXTRA_NUMBER_OF_APP_UPDATES, 0) 156 logAppDataSharingUpdatesNotificationInteraction( 157 sessionId, 158 APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION__ACTION__DISMISSED, 159 numberOfAppUpdates 160 ) 161 } 162 } 163 164 /** 165 * Called for two different jobs: the detect updates job 166 * [SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID] and the notification job 167 * [SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID]. 168 */ 169 override fun onStartJob(params: JobParameters): Boolean { 170 if (DEBUG) { 171 Log.d(LOG_TAG, "onStartJob called for job id: ${params.jobId}") 172 } 173 if (!KotlinUtils.isSafetyLabelChangeNotificationsEnabled(context)) { 174 Log.w(LOG_TAG, "Not starting job: safety label change notifications are not enabled.") 175 return false 176 } 177 if (KotlinUtils.safetyLabelChangesJobServiceKillSwitch()) { 178 Log.i(LOG_TAG, "Not starting job: kill switch is set.") 179 return false 180 } 181 when (params.jobId) { 182 SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID -> { 183 dispatchDetectUpdatesJob(params) 184 return true 185 } 186 SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID -> { 187 dispatchNotificationJob(params) 188 return true 189 } 190 else -> Log.w(LOG_TAG, "Unexpected job Id: ${params.jobId}") 191 } 192 return false 193 } 194 195 private fun dispatchDetectUpdatesJob(params: JobParameters) { 196 Log.i(LOG_TAG, "Dispatching detect updates job") 197 detectUpdatesJob = 198 GlobalScope.launch(Dispatchers.Default) { 199 try { 200 Log.i(LOG_TAG, "Detect updates job started") 201 runDetectUpdatesJob() 202 Log.i(LOG_TAG, "Detect updates job finished successfully") 203 } catch (e: Throwable) { 204 Log.e(LOG_TAG, "Detect updates job failed", e) 205 throw e 206 } finally { 207 jobFinished(params, false) 208 } 209 } 210 } 211 212 private fun dispatchNotificationJob(params: JobParameters) { 213 Log.i(LOG_TAG, "Dispatching notification job") 214 notificationJob = 215 GlobalScope.launch(Dispatchers.Default) { 216 try { 217 Log.i(LOG_TAG, "Notification job started") 218 runNotificationJob() 219 Log.i(LOG_TAG, "Notification job finished successfully") 220 } catch (e: Throwable) { 221 Log.e(LOG_TAG, "Notification job failed", e) 222 throw e 223 } finally { 224 jobFinished(params, false) 225 } 226 } 227 } 228 229 private suspend fun runDetectUpdatesJob() { 230 mutex.withLock { recordSafetyLabelsIfMissing() } 231 } 232 233 private suspend fun runNotificationJob() { 234 mutex.withLock { 235 recordSafetyLabelsIfMissing() 236 deleteSafetyLabelsNoLongerNeeded() 237 postSafetyLabelChangedNotification() 238 } 239 } 240 241 /** 242 * Records safety labels for apps that may not have propagated their safety labels to 243 * persistence through [SafetyLabelChangedBroadcastReceiver]. 244 * 245 * This is done by: 246 * 1. Initializing safety labels for apps that are relevant, but have no persisted safety labels 247 * yet. 248 * 2. Update safety labels for apps that are relevant and have persisted safety labels, if we 249 * identify that we have missed an update for them. 250 */ 251 private suspend fun recordSafetyLabelsIfMissing() { 252 val historyFile = AppsSafetyLabelHistoryPersistence.getSafetyLabelHistoryFile(context) 253 val safetyLabelsLastUpdatedTimes: Map<AppInfo, Instant> = 254 AppsSafetyLabelHistoryPersistence.getSafetyLabelsLastUpdatedTimes(historyFile) 255 // Retrieve all installed packages that are store installed on the system and 256 // that request the location permission; these are the packages that we care about for the 257 // safety labels feature. The variable name does not specify all these filters for brevity. 258 val packagesRequestingLocation: Set<Pair<String, UserHandle>> = 259 getAllStoreInstalledPackagesRequestingLocation() 260 261 val safetyLabelsToRecord = mutableSetOf<SafetyLabelForPersistence>() 262 val packageNamesWithPersistedSafetyLabels = 263 safetyLabelsLastUpdatedTimes.keys.map { it.packageName } 264 265 // Partition relevant apps by whether we already store safety labels for them. 266 val (packagesToConsiderUpdate, packagesToInitialize) = 267 packagesRequestingLocation.partition { (packageName, _) -> 268 packageName in packageNamesWithPersistedSafetyLabels 269 } 270 if (DEBUG) { 271 Log.d( 272 LOG_TAG, 273 "recording safety labels if missing:" + 274 " packagesRequestingLocation:" + 275 " $packagesRequestingLocation, packageNamesWithPersistedSafetyLabels:" + 276 " $packageNamesWithPersistedSafetyLabels" 277 ) 278 } 279 safetyLabelsToRecord.addAll(getSafetyLabels(packagesToInitialize)) 280 safetyLabelsToRecord.addAll( 281 getSafetyLabelsIfUpdatesMissed(packagesToConsiderUpdate, safetyLabelsLastUpdatedTimes) 282 ) 283 284 AppsSafetyLabelHistoryPersistence.recordSafetyLabels(safetyLabelsToRecord, historyFile) 285 } 286 287 private suspend fun getSafetyLabels( 288 packages: List<Pair<String, UserHandle>> 289 ): List<SafetyLabelForPersistence> { 290 val safetyLabelsToPersist = mutableListOf<SafetyLabelForPersistence>() 291 292 for ((packageName, user) in packages) { 293 yield() // cancellation point 294 val safetyLabelToPersist = getSafetyLabelToPersist(Pair(packageName, user)) 295 if (safetyLabelToPersist != null) { 296 safetyLabelsToPersist.add(safetyLabelToPersist) 297 } 298 } 299 return safetyLabelsToPersist 300 } 301 302 private suspend fun getSafetyLabelsIfUpdatesMissed( 303 packages: List<Pair<String, UserHandle>>, 304 safetyLabelsLastUpdatedTimes: Map<AppInfo, Instant> 305 ): List<SafetyLabelForPersistence> { 306 val safetyLabelsToPersist = mutableListOf<SafetyLabelForPersistence>() 307 for ((packageName, user) in packages) { 308 yield() // cancellation point 309 310 // If safety labels are considered up-to-date, continue as there is no need to retrieve 311 // the latest safety label; it was already captured. 312 if (areSafetyLabelsUpToDate(Pair(packageName, user), safetyLabelsLastUpdatedTimes)) { 313 continue 314 } 315 316 val safetyLabelToPersist = getSafetyLabelToPersist(Pair(packageName, user)) 317 if (safetyLabelToPersist != null) { 318 safetyLabelsToPersist.add(safetyLabelToPersist) 319 } 320 } 321 322 return safetyLabelsToPersist 323 } 324 325 /** 326 * Returns whether the provided app's safety labels are up-to-date by checking that there have 327 * been no app updates since the persisted safety label history was last updated. 328 */ 329 private suspend fun areSafetyLabelsUpToDate( 330 packageKey: Pair<String, UserHandle>, 331 safetyLabelsLastUpdatedTimes: Map<AppInfo, Instant> 332 ): Boolean { 333 val lightPackageInfo = LightPackageInfoLiveData[packageKey].getInitializedValue() 334 val lastAppUpdateTime: Instant = Instant.ofEpochMilli(lightPackageInfo?.lastUpdateTime ?: 0) 335 val latestSafetyLabelUpdateTime: Instant? = 336 safetyLabelsLastUpdatedTimes[AppInfo(packageKey.first)] 337 return latestSafetyLabelUpdateTime != null && 338 !lastAppUpdateTime.isAfter(latestSafetyLabelUpdateTime) 339 } 340 341 private suspend fun getSafetyLabelToPersist( 342 packageKey: Pair<String, UserHandle> 343 ): SafetyLabelForPersistence? { 344 val (packageName, user) = packageKey 345 346 // Get the context for the user in which the app is installed. 347 val userContext = 348 if (user == Process.myUserHandle()) { 349 context 350 } else { 351 context.createContextAsUser(user, 0) 352 } 353 354 // Asl in Apk (V+) is not supported by permissions 355 if (!SafetyLabelUtils.isAppMetadataSourceSupported(userContext, packageName)) { 356 return null 357 } 358 359 val appMetadataBundle: PersistableBundle = 360 try { 361 @Suppress("MissingPermission") 362 userContext.packageManager.getAppMetadata(packageName) 363 } catch (e: PackageManager.NameNotFoundException) { 364 Log.w(LOG_TAG, "Package $packageName not found while retrieving app metadata") 365 return null 366 } 367 val appMetadataSafetyLabel: AppMetadataSafetyLabel = 368 AppMetadataSafetyLabel.getSafetyLabelFromMetadata(appMetadataBundle) ?: return null 369 val lastUpdateTime = 370 Instant.ofEpochMilli( 371 LightPackageInfoLiveData[packageKey].getInitializedValue()?.lastUpdateTime ?: 0 372 ) 373 374 val safetyLabelForPersistence: SafetyLabelForPersistence = 375 AppsSafetyLabelHistory.SafetyLabel.extractLocationSharingSafetyLabel( 376 packageName, 377 lastUpdateTime, 378 appMetadataSafetyLabel 379 ) 380 381 return safetyLabelForPersistence 382 } 383 384 /** 385 * Deletes safety labels from persistence that are no longer necessary to persist. 386 * 387 * This is done by: 388 * 1. Deleting safety labels for apps that are no longer relevant (e.g. app not installed or app 389 * not requesting location permission). 390 * 2. Delete safety labels if there are multiple safety labels prior to the update period; at 391 * most one safety label is necessary to be persisted prior to the update period to determine 392 * updates to safety labels. 393 */ 394 private suspend fun deleteSafetyLabelsNoLongerNeeded() { 395 val historyFile = AppsSafetyLabelHistoryPersistence.getSafetyLabelHistoryFile(context) 396 val safetyLabelsLastUpdatedTimes: Map<AppInfo, Instant> = 397 AppsSafetyLabelHistoryPersistence.getSafetyLabelsLastUpdatedTimes(historyFile) 398 // Retrieve all installed packages that are store installed on the system and 399 // that request the location permission; these are the packages that we care about for the 400 // safety labels feature. The variable name does not specify all these filters for brevity. 401 val packagesRequestingLocation: Set<Pair<String, UserHandle>> = 402 getAllStoreInstalledPackagesRequestingLocation() 403 404 val packageNamesWithPersistedSafetyLabels: List<String> = 405 safetyLabelsLastUpdatedTimes.keys.map { appInfo -> appInfo.packageName } 406 val packageNamesWithRelevantSafetyLabels: List<String> = 407 packagesRequestingLocation.map { (packageName, _) -> packageName } 408 409 val appInfosToDelete: Set<AppInfo> = 410 packageNamesWithPersistedSafetyLabels 411 .filter { packageName -> packageName !in packageNamesWithRelevantSafetyLabels } 412 .map { packageName -> AppInfo(packageName) } 413 .toSet() 414 AppsSafetyLabelHistoryPersistence.deleteSafetyLabelsForApps(appInfosToDelete, historyFile) 415 416 val updatePeriod = 417 DeviceConfig.getLong( 418 DeviceConfig.NAMESPACE_PRIVACY, 419 DATA_SHARING_UPDATE_PERIOD_PROPERTY, 420 Duration.ofDays(DEFAULT_DATA_SHARING_UPDATE_PERIOD_DAYS).toMillis() 421 ) 422 AppsSafetyLabelHistoryPersistence.deleteSafetyLabelsOlderThan( 423 Instant.now().atZone(ZoneId.systemDefault()).toInstant().minusMillis(updatePeriod), 424 historyFile 425 ) 426 } 427 428 // TODO(b/261607291): Modify this logic when we enable safety label change notifications for 429 // preinstalled apps. 430 private suspend fun getAllStoreInstalledPackagesRequestingLocation(): 431 Set<Pair<String, UserHandle>> = 432 getAllPackagesRequestingLocation().filter { isSafetyLabelSupported(it) }.toSet() 433 434 private suspend fun getAllPackagesRequestingLocation(): Set<Pair<String, UserHandle>> = 435 SinglePermGroupPackagesUiInfoLiveData[Manifest.permission_group.LOCATION] 436 .getInitializedValue(staleOk = false, forceUpdate = true)!! 437 .keys 438 439 private suspend fun getAllPackagesGrantedLocation(): Set<Pair<String, UserHandle>> = 440 SinglePermGroupPackagesUiInfoLiveData[Manifest.permission_group.LOCATION] 441 .getInitializedValue(staleOk = false, forceUpdate = true)!! 442 .filter { (_, appPermGroupUiInfo) -> appPermGroupUiInfo.isPermissionGranted() } 443 .keys 444 445 private fun AppPermGroupUiInfo.isPermissionGranted() = 446 permGrantState in setOf(PERMS_ALLOWED_ALWAYS, PERMS_ALLOWED_FOREGROUND_ONLY) 447 448 private suspend fun isSafetyLabelSupported(packageUser: Pair<String, UserHandle>): Boolean { 449 val lightInstallSourceInfo = 450 LightInstallSourceInfoLiveData[packageUser].getInitializedValue() ?: return false 451 return lightInstallSourceInfo.supportsSafetyLabel 452 } 453 454 private suspend fun postSafetyLabelChangedNotification() { 455 val numberOfAppUpdates = getNumberOfAppsWithDataSharingChanged() 456 if (numberOfAppUpdates > 0) { 457 Log.i(LOG_TAG, "Showing notification: data sharing has changed") 458 showNotification(numberOfAppUpdates) 459 } else { 460 cancelNotification() 461 Log.i(LOG_TAG, "Not showing notification: data sharing has not changed") 462 } 463 } 464 465 override fun onStopJob(params: JobParameters?): Boolean { 466 if (DEBUG) { 467 Log.d(LOG_TAG, "onStopJob called for job id: ${params?.jobId}") 468 } 469 runBlocking { 470 when (params?.jobId) { 471 SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID -> { 472 Log.i(LOG_TAG, "onStopJob: cancelling detect updates job") 473 detectUpdatesJob?.cancel() 474 detectUpdatesJob = null 475 } 476 SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID -> { 477 Log.i(LOG_TAG, "onStopJob: cancelling notification job") 478 notificationJob?.cancel() 479 notificationJob = null 480 } 481 else -> Log.w(LOG_TAG, "onStopJob: unexpected job Id: ${params?.jobId}") 482 } 483 } 484 return true 485 } 486 487 /** 488 * Count the number of packages that have location granted and have location sharing updates. 489 */ 490 private suspend fun getNumberOfAppsWithDataSharingChanged(): Int { 491 val appDataSharingUpdates = 492 AppDataSharingUpdatesLiveData(PermissionControllerApplication.get()) 493 .getInitializedValue() 494 ?: return 0 495 496 return appDataSharingUpdates 497 .map { appDataSharingUpdate -> 498 val locationDataSharingUpdate = 499 appDataSharingUpdate.categorySharingUpdates[CATEGORY_LOCATION] 500 501 if (locationDataSharingUpdate == null) { 502 emptyList() 503 } else { 504 val users = 505 SinglePermGroupPackagesUiInfoLiveData[Manifest.permission_group.LOCATION] 506 .getUsersWithPermGrantedForApp(appDataSharingUpdate.packageName) 507 users 508 } 509 } 510 .flatten() 511 .count() 512 } 513 514 private fun SinglePermGroupPackagesUiInfoLiveData.getUsersWithPermGrantedForApp( 515 packageName: String 516 ): List<UserHandle> { 517 return value 518 ?.filter { 519 packageToPermInfoEntry: Map.Entry<Pair<String, UserHandle>, AppPermGroupUiInfo> -> 520 val appPermGroupUiInfo = packageToPermInfoEntry.value 521 522 appPermGroupUiInfo.isPermissionGranted() 523 } 524 ?.keys 525 ?.filter { packageUser: Pair<String, UserHandle> -> packageUser.first == packageName } 526 ?.map { packageUser: Pair<String, UserHandle> -> packageUser.second } 527 ?: listOf() 528 } 529 530 private fun AppDataSharingUpdate.containsLocationCategoryUpdate() = 531 categorySharingUpdates[CATEGORY_LOCATION] != null 532 533 private fun showNotification(numberOfAppUpdates: Int) { 534 var sessionId = INVALID_SESSION_ID 535 while (sessionId == INVALID_SESSION_ID) { 536 sessionId = random.nextLong() 537 } 538 val context = PermissionControllerApplication.get() as Context 539 val notificationManager = getSystemServiceSafe(context, NotificationManager::class.java) 540 createNotificationChannel(context, notificationManager) 541 542 val (appLabel, smallIcon, color) = KotlinUtils.getSafetyCenterNotificationResources(this) 543 val smallIconCompat = 544 IconCompat.createFromIcon(smallIcon) 545 ?: IconCompat.createWithResource(this, R.drawable.ic_info) 546 val title = context.getString(R.string.safety_label_changes_notification_title) 547 val text = context.getString(R.string.safety_label_changes_notification_desc) 548 var notificationBuilder = 549 NotificationCompat.Builder(context, PERMISSION_REMINDER_CHANNEL_ID) 550 .setColor(color) 551 .setSmallIcon(smallIconCompat) 552 .setContentTitle(title) 553 .setContentText(text) 554 .setPriority(NotificationCompat.PRIORITY_DEFAULT) 555 .setLocalOnly(true) 556 .setAutoCancel(true) 557 .setSilent(true) 558 .setContentIntent(createIntentToOpenAppDataSharingUpdates(context, sessionId)) 559 .setDeleteIntent( 560 createIntentToLogDismissNotificationEvent( 561 context, 562 sessionId, 563 numberOfAppUpdates 564 ) 565 ) 566 notificationBuilder.addExtras( 567 Bundle().apply { putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, appLabel) } 568 ) 569 570 notificationManager.notify( 571 SAFETY_LABEL_CHANGES_NOTIFICATION_ID, 572 notificationBuilder.build() 573 ) 574 575 logAppDataSharingUpdatesNotificationInteraction( 576 sessionId, 577 APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_SHOWN, 578 numberOfAppUpdates 579 ) 580 Log.v(LOG_TAG, "Safety label change notification sent.") 581 } 582 583 private fun cancelNotification() { 584 val notificationManager = getSystemServiceSafe(context, NotificationManager::class.java) 585 notificationManager.cancel(SAFETY_LABEL_CHANGES_NOTIFICATION_ID) 586 Log.v(LOG_TAG, "Safety label change notification cancelled.") 587 } 588 589 private fun createIntentToOpenAppDataSharingUpdates( 590 context: Context, 591 sessionId: Long 592 ): PendingIntent { 593 return PendingIntent.getActivity( 594 context, 595 0, 596 Intent(Intent.ACTION_REVIEW_APP_DATA_SHARING_UPDATES).apply { 597 putExtra(EXTRA_SESSION_ID, sessionId) 598 }, 599 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 600 ) 601 } 602 603 private fun createIntentToLogDismissNotificationEvent( 604 context: Context, 605 sessionId: Long, 606 numberOfAppUpdates: Int 607 ): PendingIntent { 608 return PendingIntent.getBroadcast( 609 context, 610 0, 611 Intent(context, NotificationDeleteHandler::class.java).apply { 612 putExtra(EXTRA_SESSION_ID, sessionId) 613 putExtra(EXTRA_NUMBER_OF_APP_UPDATES, numberOfAppUpdates) 614 }, 615 PendingIntent.FLAG_ONE_SHOT or 616 PendingIntent.FLAG_UPDATE_CURRENT or 617 PendingIntent.FLAG_IMMUTABLE 618 ) 619 } 620 621 private fun createNotificationChannel( 622 context: Context, 623 notificationManager: NotificationManager 624 ) { 625 val notificationChannel = 626 NotificationChannel( 627 PERMISSION_REMINDER_CHANNEL_ID, 628 context.getString(R.string.permission_reminders), 629 NotificationManager.IMPORTANCE_LOW 630 ) 631 632 notificationManager.createNotificationChannel(notificationChannel) 633 } 634 635 companion object { 636 private val LOG_TAG = SafetyLabelChangesJobService::class.java.simpleName 637 private const val DEBUG = true 638 639 private const val ACTION_SET_UP_SAFETY_LABEL_CHANGES_JOB = 640 "com.android.permissioncontroller.action.SET_UP_SAFETY_LABEL_CHANGES_JOB" 641 private const val EXTRA_NUMBER_OF_APP_UPDATES = 642 "com.android.permissioncontroller.extra.NUMBER_OF_APP_UPDATES" 643 644 private const val DATA_SHARING_UPDATE_PERIOD_PROPERTY = "data_sharing_update_period_millis" 645 private const val DEFAULT_DATA_SHARING_UPDATE_PERIOD_DAYS: Long = 30 646 647 private fun scheduleDetectUpdatesJob(context: Context) { 648 try { 649 val jobScheduler = getSystemServiceSafe(context, JobScheduler::class.java) 650 651 if ( 652 jobScheduler.getPendingJob(SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID) != null 653 ) { 654 Log.i(LOG_TAG, "Not scheduling detect updates job: already scheduled.") 655 return 656 } 657 658 val job = 659 JobInfo.Builder( 660 SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID, 661 ComponentName(context, SafetyLabelChangesJobService::class.java) 662 ) 663 .setRequiresDeviceIdle( 664 KotlinUtils.runSafetyLabelChangesJobOnlyWhenDeviceIdle() 665 ) 666 .build() 667 val result = jobScheduler.schedule(job) 668 if (result != JobScheduler.RESULT_SUCCESS) { 669 Log.w(LOG_TAG, "Detect updates job not scheduled, result code: $result") 670 } else { 671 Log.i(LOG_TAG, "Detect updates job scheduled successfully.") 672 } 673 } catch (e: Throwable) { 674 Log.e(LOG_TAG, "Failed to schedule detect updates job", e) 675 throw e 676 } 677 } 678 679 private fun schedulePeriodicNotificationJob(context: Context) { 680 try { 681 val jobScheduler = getSystemServiceSafe(context, JobScheduler::class.java) 682 if ( 683 jobScheduler.getPendingJob(SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID) != 684 null 685 ) { 686 Log.i(LOG_TAG, "Not scheduling notification job: already scheduled.") 687 return 688 } 689 690 val job = 691 @Suppress("MissingPermission") 692 JobInfo.Builder( 693 SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID, 694 ComponentName(context, SafetyLabelChangesJobService::class.java) 695 ) 696 .setRequiresDeviceIdle( 697 KotlinUtils.runSafetyLabelChangesJobOnlyWhenDeviceIdle() 698 ) 699 .setPeriodic(KotlinUtils.getSafetyLabelChangesJobIntervalMillis()) 700 .setPersisted(true) 701 .build() 702 val result = jobScheduler.schedule(job) 703 if (result != JobScheduler.RESULT_SUCCESS) { 704 Log.w(LOG_TAG, "Notification job not scheduled, result code: $result") 705 } else { 706 Log.i(LOG_TAG, "Notification job scheduled successfully.") 707 } 708 } catch (e: Throwable) { 709 Log.e(LOG_TAG, "Failed to schedule notification job", e) 710 throw e 711 } 712 } 713 714 private fun logAppDataSharingUpdatesNotificationInteraction( 715 sessionId: Long, 716 interactionType: Int, 717 numberOfAppUpdates: Int 718 ) { 719 PermissionControllerStatsLog.write( 720 APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION, 721 sessionId, 722 interactionType, 723 numberOfAppUpdates 724 ) 725 Log.v( 726 LOG_TAG, 727 "Notification interaction occurred with" + 728 " sessionId=$sessionId" + 729 " action=$interactionType" + 730 " numberOfAppUpdates=$numberOfAppUpdates" 731 ) 732 } 733 } 734 } 735