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 (isContextInProfileUser(receiverContext)) { 115 Log.i( 116 LOG_TAG, 117 "onReceive: Received broadcast in profile, not scheduling safety label" + 118 " change job" 119 ) 120 return 121 } 122 if ( 123 intent.action != ACTION_BOOT_COMPLETED && 124 intent.action != ACTION_SET_UP_SAFETY_LABEL_CHANGES_JOB 125 ) { 126 return 127 } 128 scheduleDetectUpdatesJob(receiverContext) 129 schedulePeriodicNotificationJob(receiverContext) 130 } 131 132 private fun isContextInProfileUser(context: Context): Boolean { 133 val userManager: UserManager = context.getSystemService(UserManager::class.java)!! 134 return userManager.isProfile 135 } 136 } 137 138 /** Handle the case where the notification is swiped away without further interaction. */ 139 class NotificationDeleteHandler : BroadcastReceiver() { 140 override fun onReceive(receiverContext: Context, intent: Intent) { 141 Log.d(LOG_TAG, "NotificationDeleteHandler: received broadcast") 142 if (!KotlinUtils.isSafetyLabelChangeNotificationsEnabled(receiverContext)) { 143 Log.i( 144 LOG_TAG, 145 "NotificationDeleteHandler: " + 146 "safety label change notifications are not enabled." 147 ) 148 return 149 } 150 val sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID) 151 val numberOfAppUpdates = intent.getIntExtra(EXTRA_NUMBER_OF_APP_UPDATES, 0) 152 logAppDataSharingUpdatesNotificationInteraction( 153 sessionId, 154 APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION__ACTION__DISMISSED, 155 numberOfAppUpdates 156 ) 157 } 158 } 159 160 /** 161 * Called for two different jobs: the detect updates job 162 * [SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID] and the notification job 163 * [SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID]. 164 */ 165 override fun onStartJob(params: JobParameters): Boolean { 166 if (DEBUG) { 167 Log.d(LOG_TAG, "onStartJob called for job id: ${params.jobId}") 168 } 169 if (!KotlinUtils.isSafetyLabelChangeNotificationsEnabled(context)) { 170 Log.w(LOG_TAG, "Not starting job: safety label change notifications are not enabled.") 171 return false 172 } 173 when (params.jobId) { 174 SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID -> { 175 dispatchDetectUpdatesJob(params) 176 return true 177 } 178 SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID -> { 179 dispatchNotificationJob(params) 180 return true 181 } 182 else -> Log.w(LOG_TAG, "Unexpected job Id: ${params.jobId}") 183 } 184 return false 185 } 186 187 private fun dispatchDetectUpdatesJob(params: JobParameters) { 188 Log.i(LOG_TAG, "Dispatching detect updates job") 189 detectUpdatesJob = 190 GlobalScope.launch(Dispatchers.Default) { 191 try { 192 Log.i(LOG_TAG, "Detect updates job started") 193 runDetectUpdatesJob() 194 Log.i(LOG_TAG, "Detect updates job finished successfully") 195 } catch (e: Throwable) { 196 Log.e(LOG_TAG, "Detect updates job failed", e) 197 throw e 198 } finally { 199 jobFinished(params, false) 200 } 201 } 202 } 203 204 private fun dispatchNotificationJob(params: JobParameters) { 205 Log.i(LOG_TAG, "Dispatching notification job") 206 notificationJob = 207 GlobalScope.launch(Dispatchers.Default) { 208 try { 209 Log.i(LOG_TAG, "Notification job started") 210 runNotificationJob() 211 Log.i(LOG_TAG, "Notification job finished successfully") 212 } catch (e: Throwable) { 213 Log.e(LOG_TAG, "Notification job failed", e) 214 throw e 215 } finally { 216 jobFinished(params, false) 217 } 218 } 219 } 220 221 private suspend fun runDetectUpdatesJob() { 222 mutex.withLock { recordSafetyLabelsIfMissing() } 223 } 224 225 private suspend fun runNotificationJob() { 226 mutex.withLock { 227 recordSafetyLabelsIfMissing() 228 deleteSafetyLabelsNoLongerNeeded() 229 postSafetyLabelChangedNotification() 230 } 231 } 232 233 /** 234 * Records safety labels for apps that may not have propagated their safety labels to 235 * persistence through [SafetyLabelChangedBroadcastReceiver]. 236 * 237 * This is done by: 238 * 1. Initializing safety labels for apps that are relevant, but have no persisted safety labels 239 * yet. 240 * 2. Update safety labels for apps that are relevant and have persisted safety labels, if we 241 * identify that we have missed an update for them. 242 */ 243 private suspend fun recordSafetyLabelsIfMissing() { 244 val historyFile = AppsSafetyLabelHistoryPersistence.getSafetyLabelHistoryFile(context) 245 val safetyLabelsLastUpdatedTimes: Map<AppInfo, Instant> = 246 AppsSafetyLabelHistoryPersistence.getSafetyLabelsLastUpdatedTimes(historyFile) 247 // Retrieve all installed packages that are store installed on the system and 248 // that request the location permission; these are the packages that we care about for the 249 // safety labels feature. The variable name does not specify all these filters for brevity. 250 val packagesRequestingLocation: Set<Pair<String, UserHandle>> = 251 getAllStoreInstalledPackagesRequestingLocation() 252 253 val safetyLabelsToRecord = mutableSetOf<SafetyLabelForPersistence>() 254 val packageNamesWithPersistedSafetyLabels = 255 safetyLabelsLastUpdatedTimes.keys.map { it.packageName } 256 257 // Partition relevant apps by whether we already store safety labels for them. 258 val (packagesToConsiderUpdate, packagesToInitialize) = 259 packagesRequestingLocation.partition { (packageName, _) -> 260 packageName in packageNamesWithPersistedSafetyLabels 261 } 262 if (DEBUG) { 263 Log.d( 264 LOG_TAG, 265 "recording safety labels if missing:" + 266 " packagesRequestingLocation:" + 267 " $packagesRequestingLocation, packageNamesWithPersistedSafetyLabels:" + 268 " $packageNamesWithPersistedSafetyLabels" 269 ) 270 } 271 safetyLabelsToRecord.addAll(getSafetyLabels(packagesToInitialize)) 272 safetyLabelsToRecord.addAll( 273 getSafetyLabelsIfUpdatesMissed(packagesToConsiderUpdate, safetyLabelsLastUpdatedTimes) 274 ) 275 276 AppsSafetyLabelHistoryPersistence.recordSafetyLabels(safetyLabelsToRecord, historyFile) 277 } 278 279 private suspend fun getSafetyLabels( 280 packages: List<Pair<String, UserHandle>> 281 ): List<SafetyLabelForPersistence> { 282 val safetyLabelsToPersist = mutableListOf<SafetyLabelForPersistence>() 283 284 for ((packageName, user) in packages) { 285 yield() // cancellation point 286 val safetyLabelToPersist = getSafetyLabelToPersist(Pair(packageName, user)) 287 if (safetyLabelToPersist != null) { 288 safetyLabelsToPersist.add(safetyLabelToPersist) 289 } 290 } 291 return safetyLabelsToPersist 292 } 293 294 private suspend fun getSafetyLabelsIfUpdatesMissed( 295 packages: List<Pair<String, UserHandle>>, 296 safetyLabelsLastUpdatedTimes: Map<AppInfo, Instant> 297 ): List<SafetyLabelForPersistence> { 298 val safetyLabelsToPersist = mutableListOf<SafetyLabelForPersistence>() 299 for ((packageName, user) in packages) { 300 yield() // cancellation point 301 302 // If safety labels are considered up-to-date, continue as there is no need to retrieve 303 // the latest safety label; it was already captured. 304 if (areSafetyLabelsUpToDate(Pair(packageName, user), safetyLabelsLastUpdatedTimes)) { 305 continue 306 } 307 308 val safetyLabelToPersist = getSafetyLabelToPersist(Pair(packageName, user)) 309 if (safetyLabelToPersist != null) { 310 safetyLabelsToPersist.add(safetyLabelToPersist) 311 } 312 } 313 314 return safetyLabelsToPersist 315 } 316 317 /** 318 * Returns whether the provided app's safety labels are up-to-date by checking that there have 319 * been no app updates since the persisted safety label history was last updated. 320 */ 321 private suspend fun areSafetyLabelsUpToDate( 322 packageKey: Pair<String, UserHandle>, 323 safetyLabelsLastUpdatedTimes: Map<AppInfo, Instant> 324 ): Boolean { 325 val lightPackageInfo = LightPackageInfoLiveData[packageKey].getInitializedValue() 326 val lastAppUpdateTime: Instant = Instant.ofEpochMilli(lightPackageInfo?.lastUpdateTime ?: 0) 327 val latestSafetyLabelUpdateTime: Instant? = 328 safetyLabelsLastUpdatedTimes[AppInfo(packageKey.first)] 329 return latestSafetyLabelUpdateTime != null && 330 !lastAppUpdateTime.isAfter(latestSafetyLabelUpdateTime) 331 } 332 333 private suspend fun getSafetyLabelToPersist( 334 packageKey: Pair<String, UserHandle> 335 ): SafetyLabelForPersistence? { 336 val (packageName, user) = packageKey 337 338 // Get the context for the user in which the app is installed. 339 val userContext = 340 if (user == Process.myUserHandle()) { 341 context 342 } else { 343 context.createContextAsUser(user, 0) 344 } 345 346 // Asl in Apk (V+) is not supported by permissions 347 if (!SafetyLabelUtils.isAppMetadataSourceSupported(userContext, packageName)) { 348 return null 349 } 350 351 val appMetadataBundle: PersistableBundle = 352 try { 353 @Suppress("MissingPermission") 354 userContext.packageManager.getAppMetadata(packageName) 355 } catch (e: PackageManager.NameNotFoundException) { 356 Log.w(LOG_TAG, "Package $packageName not found while retrieving app metadata") 357 return null 358 } 359 val appMetadataSafetyLabel: AppMetadataSafetyLabel = 360 AppMetadataSafetyLabel.getSafetyLabelFromMetadata(appMetadataBundle) ?: return null 361 val lastUpdateTime = 362 Instant.ofEpochMilli( 363 LightPackageInfoLiveData[packageKey].getInitializedValue()?.lastUpdateTime ?: 0 364 ) 365 366 val safetyLabelForPersistence: SafetyLabelForPersistence = 367 AppsSafetyLabelHistory.SafetyLabel.extractLocationSharingSafetyLabel( 368 packageName, 369 lastUpdateTime, 370 appMetadataSafetyLabel 371 ) 372 373 return safetyLabelForPersistence 374 } 375 376 /** 377 * Deletes safety labels from persistence that are no longer necessary to persist. 378 * 379 * This is done by: 380 * 1. Deleting safety labels for apps that are no longer relevant (e.g. app not installed or app 381 * not requesting location permission). 382 * 2. Delete safety labels if there are multiple safety labels prior to the update period; at 383 * most one safety label is necessary to be persisted prior to the update period to determine 384 * updates to safety labels. 385 */ 386 private suspend fun deleteSafetyLabelsNoLongerNeeded() { 387 val historyFile = AppsSafetyLabelHistoryPersistence.getSafetyLabelHistoryFile(context) 388 val safetyLabelsLastUpdatedTimes: Map<AppInfo, Instant> = 389 AppsSafetyLabelHistoryPersistence.getSafetyLabelsLastUpdatedTimes(historyFile) 390 // Retrieve all installed packages that are store installed on the system and 391 // that request the location permission; these are the packages that we care about for the 392 // safety labels feature. The variable name does not specify all these filters for brevity. 393 val packagesRequestingLocation: Set<Pair<String, UserHandle>> = 394 getAllStoreInstalledPackagesRequestingLocation() 395 396 val packageNamesWithPersistedSafetyLabels: List<String> = 397 safetyLabelsLastUpdatedTimes.keys.map { appInfo -> appInfo.packageName } 398 val packageNamesWithRelevantSafetyLabels: List<String> = 399 packagesRequestingLocation.map { (packageName, _) -> packageName } 400 401 val appInfosToDelete: Set<AppInfo> = 402 packageNamesWithPersistedSafetyLabels 403 .filter { packageName -> packageName !in packageNamesWithRelevantSafetyLabels } 404 .map { packageName -> AppInfo(packageName) } 405 .toSet() 406 AppsSafetyLabelHistoryPersistence.deleteSafetyLabelsForApps(appInfosToDelete, historyFile) 407 408 val updatePeriod = 409 DeviceConfig.getLong( 410 DeviceConfig.NAMESPACE_PRIVACY, 411 DATA_SHARING_UPDATE_PERIOD_PROPERTY, 412 Duration.ofDays(DEFAULT_DATA_SHARING_UPDATE_PERIOD_DAYS).toMillis() 413 ) 414 AppsSafetyLabelHistoryPersistence.deleteSafetyLabelsOlderThan( 415 Instant.now().atZone(ZoneId.systemDefault()).toInstant().minusMillis(updatePeriod), 416 historyFile 417 ) 418 } 419 420 // TODO(b/261607291): Modify this logic when we enable safety label change notifications for 421 // preinstalled apps. 422 private suspend fun getAllStoreInstalledPackagesRequestingLocation(): 423 Set<Pair<String, UserHandle>> = 424 getAllPackagesRequestingLocation().filter { isSafetyLabelSupported(it) }.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() ?: return false 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() ?: return 0 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 } ?: listOf() 518 } 519 520 private fun AppDataSharingUpdate.containsLocationCategoryUpdate() = 521 categorySharingUpdates[CATEGORY_LOCATION] != null 522 523 private fun showNotification(numberOfAppUpdates: Int) { 524 var sessionId = INVALID_SESSION_ID 525 while (sessionId == INVALID_SESSION_ID) { 526 sessionId = random.nextLong() 527 } 528 val context = PermissionControllerApplication.get() as Context 529 val notificationManager = getSystemServiceSafe(context, NotificationManager::class.java) 530 createNotificationChannel(context, notificationManager) 531 532 val (appLabel, smallIcon, color) = KotlinUtils.getSafetyCenterNotificationResources(this) 533 val smallIconCompat = 534 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(true) 654 .build() 655 val result = jobScheduler.schedule(job) 656 if (result != JobScheduler.RESULT_SUCCESS) { 657 Log.w(LOG_TAG, "Detect updates job not scheduled, result code: $result") 658 } else { 659 Log.i(LOG_TAG, "Detect updates job scheduled successfully.") 660 } 661 } catch (e: Throwable) { 662 Log.e(LOG_TAG, "Failed to schedule detect updates job", e) 663 throw e 664 } 665 } 666 667 private fun schedulePeriodicNotificationJob(context: Context) { 668 try { 669 val jobScheduler = getSystemServiceSafe(context, JobScheduler::class.java) 670 if ( 671 jobScheduler.getPendingJob(SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID) != 672 null 673 ) { 674 Log.i(LOG_TAG, "Not scheduling notification job: already scheduled.") 675 return 676 } 677 678 val job = 679 @Suppress("MissingPermission") 680 JobInfo.Builder( 681 SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID, 682 ComponentName(context, SafetyLabelChangesJobService::class.java) 683 ) 684 .setRequiresDeviceIdle(true) 685 .setPeriodic(KotlinUtils.getSafetyLabelChangesJobIntervalMillis()) 686 .setPersisted(true) 687 .build() 688 val result = jobScheduler.schedule(job) 689 if (result != JobScheduler.RESULT_SUCCESS) { 690 Log.w(LOG_TAG, "Notification job not scheduled, result code: $result") 691 } else { 692 Log.i(LOG_TAG, "Notification job scheduled successfully.") 693 } 694 } catch (e: Throwable) { 695 Log.e(LOG_TAG, "Failed to schedule notification job", e) 696 throw e 697 } 698 } 699 700 private fun logAppDataSharingUpdatesNotificationInteraction( 701 sessionId: Long, 702 interactionType: Int, 703 numberOfAppUpdates: Int 704 ) { 705 PermissionControllerStatsLog.write( 706 APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION, 707 sessionId, 708 interactionType, 709 numberOfAppUpdates 710 ) 711 Log.v( 712 LOG_TAG, 713 "Notification interaction occurred with" + 714 " sessionId=$sessionId" + 715 " action=$interactionType" + 716 " numberOfAppUpdates=$numberOfAppUpdates" 717 ) 718 } 719 } 720 } 721