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