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