• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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