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