1 /*
<lambda>null2 * Copyright (C) 2022 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.privacysources
18
19 import android.app.Notification
20 import android.app.NotificationChannel
21 import android.app.NotificationManager
22 import android.app.PendingIntent
23 import android.app.PendingIntent.FLAG_IMMUTABLE
24 import android.app.PendingIntent.FLAG_ONE_SHOT
25 import android.app.PendingIntent.FLAG_UPDATE_CURRENT
26 import android.app.job.JobInfo
27 import android.app.job.JobParameters
28 import android.app.job.JobScheduler
29 import android.app.job.JobService
30 import android.app.role.RoleManager
31 import android.content.BroadcastReceiver
32 import android.content.ComponentName
33 import android.content.Context
34 import android.content.Context.MODE_PRIVATE
35 import android.content.Intent
36 import android.content.Intent.EXTRA_COMPONENT_NAME
37 import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
38 import android.content.Intent.FLAG_RECEIVER_FOREGROUND
39 import android.content.SharedPreferences
40 import android.content.pm.PackageInfo
41 import android.content.pm.PackageManager
42 import android.os.Build
43 import android.os.Bundle
44 import android.provider.DeviceConfig
45 import android.provider.Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS
46 import android.provider.Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME
47 import android.safetycenter.SafetyCenterManager
48 import android.safetycenter.SafetyCenterManager.EXTRA_SAFETY_SOURCE_ID
49 import android.safetycenter.SafetyCenterManager.EXTRA_SAFETY_SOURCE_ISSUE_ID
50 import android.safetycenter.SafetyEvent
51 import android.safetycenter.SafetySourceData
52 import android.safetycenter.SafetySourceIssue
53 import android.service.notification.StatusBarNotification
54 import android.util.Log
55 import androidx.annotation.ChecksSdkIntAtLeast
56 import androidx.annotation.GuardedBy
57 import androidx.annotation.RequiresApi
58 import androidx.annotation.VisibleForTesting
59 import androidx.annotation.WorkerThread
60 import com.android.modules.utils.build.SdkLevel
61 import com.android.permissioncontroller.Constants
62 import com.android.permissioncontroller.Constants.KEY_LAST_NOTIFICATION_LISTENER_NOTIFICATION_SHOWN
63 import com.android.permissioncontroller.Constants.NOTIFICATION_LISTENER_CHECK_NOTIFICATION_ID
64 import com.android.permissioncontroller.Constants.PERIODIC_NOTIFICATION_LISTENER_CHECK_JOB_ID
65 import com.android.permissioncontroller.PermissionControllerStatsLog
66 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION
67 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CARD_DISMISSED
68 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CLICKED_CTA1
69 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__NOTIFICATION_LISTENER
70 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION
71 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__DISMISSED
72 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_SHOWN
73 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__NOTIFICATION_LISTENER
74 import com.android.permissioncontroller.R
75 import com.android.permissioncontroller.permission.utils.KotlinUtils
76 import com.android.permissioncontroller.permission.utils.Utils
77 import com.android.permissioncontroller.permission.utils.Utils.getSystemServiceSafe
78 import com.android.permissioncontroller.privacysources.SafetyCenterReceiver.RefreshEvent
79 import java.lang.System.currentTimeMillis
80 import java.util.Random
81 import java.util.concurrent.TimeUnit.DAYS
82 import java.util.function.BooleanSupplier
83 import kotlinx.coroutines.Dispatchers.Default
84 import kotlinx.coroutines.GlobalScope
85 import kotlinx.coroutines.Job
86 import kotlinx.coroutines.launch
87 import kotlinx.coroutines.sync.Mutex
88 import kotlinx.coroutines.sync.withLock
89
90 private val TAG = "NotificationListenerCheck"
91 private const val DEBUG = false
92 const val SC_NLS_SOURCE_ID = "AndroidNotificationListener"
93 @VisibleForTesting const val SC_NLS_DISABLE_ACTION_ID = "disable_nls_component"
94
95 /** Device config property for whether notification listener check is enabled on the device */
96 @VisibleForTesting
97 const val PROPERTY_NOTIFICATION_LISTENER_CHECK_ENABLED = "notification_listener_check_enabled"
98
99 /**
100 * Device config property for time period in milliseconds after which current enabled notification
101 * listeners are queried
102 */
103 private const val PROPERTY_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS =
104 "notification_listener_check_interval_millis"
105
106 private val DEFAULT_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS = DAYS.toMillis(1)
107
108 private fun isNotificationListenerCheckFlagEnabled(): Boolean {
109 // TODO: b/249789657 Set default to true after policy exemption + impact analysis
110 return DeviceConfig.getBoolean(
111 DeviceConfig.NAMESPACE_PRIVACY,
112 PROPERTY_NOTIFICATION_LISTENER_CHECK_ENABLED,
113 false
114 )
115 }
116
117 /**
118 * Get time in between two periodic checks.
119 *
120 * Default: 1 day
121 *
122 * @return The time in between check in milliseconds
123 */
getPeriodicCheckIntervalMillisnull124 private fun getPeriodicCheckIntervalMillis(): Long {
125 return DeviceConfig.getLong(
126 DeviceConfig.NAMESPACE_PRIVACY,
127 PROPERTY_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS,
128 DEFAULT_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS
129 )
130 }
131
132 /**
133 * Flexibility of the periodic check.
134 *
135 * 10% of [.getPeriodicCheckIntervalMillis]
136 *
137 * @return The flexibility of the periodic check in milliseconds
138 */
getFlexForPeriodicCheckMillisnull139 private fun getFlexForPeriodicCheckMillis(): Long {
140 return getPeriodicCheckIntervalMillis() / 10
141 }
142
143 /**
144 * Minimum time in between showing two notifications.
145 *
146 * This is just small enough so that the periodic check can always show a notification.
147 *
148 * @return The minimum time in milliseconds
149 */
getInBetweenNotificationsMillisnull150 private fun getInBetweenNotificationsMillis(): Long {
151 return getPeriodicCheckIntervalMillis() - (getFlexForPeriodicCheckMillis() * 2.1).toLong()
152 }
153
154 /** Notification Listener Check requires Android T or later */
155 @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
checkNotificationListenerCheckSupportednull156 private fun checkNotificationListenerCheckSupported(): Boolean {
157 return SdkLevel.isAtLeastT()
158 }
159
160 /**
161 * Returns {@code true} when Notification listener check is supported, feature flag enabled and
162 * Safety Center enabled
163 */
164 @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
checkNotificationListenerCheckEnablednull165 private fun checkNotificationListenerCheckEnabled(context: Context): Boolean {
166 return checkNotificationListenerCheckSupported() &&
167 isNotificationListenerCheckFlagEnabled() &&
168 getSystemServiceSafe(context, SafetyCenterManager::class.java).isSafetyCenterEnabled
169 }
170
getSafetySourceIssueIdFromComponentNamenull171 private fun getSafetySourceIssueIdFromComponentName(componentName: ComponentName): String {
172 return "notification_listener_${componentName.flattenToString()}"
173 }
174
175 /**
176 * Show notification that double-guesses the user if they really wants to grant notification
177 * listener permission to an app.
178 *
179 * <p>A notification is scheduled periodically, or on demand
180 *
181 * <p>We rate limit the number of notification we show and only ever show one notification at a
182 * time.
183 *
184 * <p>As there are many cases why a notification should not been shown, we always schedule a
185 * {@link #addNotificationListenerNotificationIfNeeded check} which then might add a notification.
186 *
187 * @param context Used to resolve managers
188 * @param shouldCancel If supplied, can be used to interrupt long-running operations
189 */
190 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
191 @VisibleForTesting
192 class NotificationListenerCheckInternal(
193 context: Context,
194 private val shouldCancel: BooleanSupplier?
195 ) {
196 private val parentUserContext = Utils.getParentUserContext(context)
197 private val random = Random()
198 private val sharedPrefs: SharedPreferences =
199 parentUserContext.getSharedPreferences(NLS_PREFERENCE_FILE, MODE_PRIVATE)
200
201 // Don't initialize until used. Delegate used for testing
202 @VisibleForTesting
<lambda>null203 val exemptPackagesDelegate = lazy {
204 getExemptedPackages(
205 getSystemServiceSafe(parentUserContext, RoleManager::class.java),
206 parentUserContext
207 )
208 }
209 @VisibleForTesting val exemptPackages: Set<String> by exemptPackagesDelegate
210
211 companion object {
212 @VisibleForTesting const val NLS_PREFERENCE_FILE = "nls_preference"
213 private const val KEY_ALREADY_NOTIFIED_COMPONENTS = "already_notified_services"
214
215 @VisibleForTesting const val SC_NLS_ISSUE_TYPE_ID = "notification_listener_privacy_issue"
216 @VisibleForTesting
217 const val SC_SHOW_NLS_SETTINGS_ACTION_ID = "show_notification_listener_settings"
218
219 private const val SYSTEM_PKG = "android"
220
221 private const val SYSTEM_AMBIENT_AUDIO_INTELLIGENCE =
222 "android.app.role.SYSTEM_AMBIENT_AUDIO_INTELLIGENCE"
223 private const val SYSTEM_UI_INTELLIGENCE = "android.app.role.SYSTEM_UI_INTELLIGENCE"
224 private const val SYSTEM_AUDIO_INTELLIGENCE = "android.app.role.SYSTEM_AUDIO_INTELLIGENCE"
225 private const val SYSTEM_NOTIFICATION_INTELLIGENCE =
226 "android.app.role.SYSTEM_NOTIFICATION_INTELLIGENCE"
227 private const val SYSTEM_TEXT_INTELLIGENCE = "android.app.role.SYSTEM_TEXT_INTELLIGENCE"
228 private const val SYSTEM_VISUAL_INTELLIGENCE = "android.app.role.SYSTEM_VISUAL_INTELLIGENCE"
229
230 // This excludes System intelligence roles
231 private val EXEMPTED_ROLES =
232 arrayOf(
233 SYSTEM_AMBIENT_AUDIO_INTELLIGENCE,
234 SYSTEM_UI_INTELLIGENCE,
235 SYSTEM_AUDIO_INTELLIGENCE,
236 SYSTEM_NOTIFICATION_INTELLIGENCE,
237 SYSTEM_TEXT_INTELLIGENCE,
238 SYSTEM_VISUAL_INTELLIGENCE
239 )
240
241 /** Lock required for all public methods */
242 private val nlsLock = Mutex()
243
244 /** lock for shared preferences writes */
245 private val sharedPrefsLock = Mutex()
246
247 private val sourceStateChangedSafetyEvent =
248 SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED).build()
249 }
250
251 /**
252 * Check for enabled notification listeners and notify user if needed.
253 *
254 * <p>Always run async inside a {@NotificationListenerCheckJobService} via coroutine.
255 */
256 @WorkerThread
getEnabledNotificationListenersAndNotifyIfNeedednull257 suspend fun getEnabledNotificationListenersAndNotifyIfNeeded(
258 params: JobParameters,
259 service: NotificationListenerCheckJobService
260 ) {
261 nlsLock.withLock {
262 try {
263 getEnabledNotificationListenersAndNotifyIfNeededLocked()
264 service.jobFinished(params, false)
265 } catch (e: Exception) {
266 Log.e(TAG, "Could not check for notification listeners", e)
267 service.jobFinished(params, true)
268 } finally {
269 service.clearJob()
270 }
271 }
272 }
273
274 @Throws(InterruptedException::class)
getEnabledNotificationListenersAndNotifyIfNeededLockednull275 private suspend fun getEnabledNotificationListenersAndNotifyIfNeededLocked() {
276 val enabledComponents: List<ComponentName> = getEnabledNotificationListeners()
277
278 // Clear disabled but previously notified components from notified components data
279 removeDisabledComponentsFromNotifiedComponents(enabledComponents)
280 val notifiedComponents =
281 getNotifiedComponents().mapNotNull { ComponentName.unflattenFromString(it) }
282
283 // Filter to unnotified components
284 val unNotifiedComponents = enabledComponents.filter { it !in notifiedComponents }
285 var sessionId = Constants.INVALID_SESSION_ID
286 while (sessionId == Constants.INVALID_SESSION_ID) {
287 sessionId = random.nextLong()
288 }
289 if (DEBUG) {
290 Log.d(
291 TAG,
292 "Found ${enabledComponents.size} enabled notification listeners. " +
293 "${notifiedComponents.size} already notified. ${unNotifiedComponents.size} " +
294 "unnotified, sessionId = $sessionId"
295 )
296 }
297
298 throwInterruptedExceptionIfTaskIsCanceled()
299
300 postSystemNotificationIfNeeded(unNotifiedComponents, sessionId)
301 sendIssuesToSafetyCenter(enabledComponents, sessionId)
302 }
303
304 /**
305 * Get the [components][ComponentName] which have enabled notification listeners for the
306 * parent/context user. Excludes exempt packages.
307 *
308 * @throws InterruptedException If [.shouldCancel]
309 */
310 @Throws(InterruptedException::class)
getEnabledNotificationListenersnull311 private fun getEnabledNotificationListeners(): List<ComponentName> {
312 // Get all enabled NotificationListenerService components for primary user. NLS from managed
313 // profiles are never bound.
314 val enabledNotificationListeners =
315 getSystemServiceSafe(parentUserContext, NotificationManager::class.java)
316 .enabledNotificationListeners
317
318 // Filter to components not in exempt packages
319 val enabledNotificationListenersExcludingExemptPackages =
320 enabledNotificationListeners.filter { !exemptPackages.contains(it.packageName) }
321
322 if (DEBUG) {
323 Log.d(
324 TAG,
325 "enabledNotificationListeners=$enabledNotificationListeners\n" +
326 "enabledNotificationListenersExcludingExemptPackages=" +
327 "$enabledNotificationListenersExcludingExemptPackages"
328 )
329 }
330
331 throwInterruptedExceptionIfTaskIsCanceled()
332 return enabledNotificationListenersExcludingExemptPackages
333 }
334
335 /** Get all the exempted packages. */
getExemptedPackagesnull336 private fun getExemptedPackages(roleManager: RoleManager, context: Context): Set<String> {
337 val exemptedPackages: MutableSet<String> = HashSet()
338 exemptedPackages.add(SYSTEM_PKG)
339 EXEMPTED_ROLES.forEach { role -> exemptedPackages.addAll(roleManager.getRoleHolders(role)) }
340 exemptedPackages.addAll(NotificationListenerPregrants(context).pregrantedPackages)
341 return exemptedPackages
342 }
343
344 @VisibleForTesting
getNotifiedComponentsnull345 fun getNotifiedComponents(): MutableSet<String> {
346 return sharedPrefs.getStringSet(KEY_ALREADY_NOTIFIED_COMPONENTS, mutableSetOf<String>())!!
347 }
348
removeDisabledComponentsFromNotifiedComponentsnull349 suspend fun removeDisabledComponentsFromNotifiedComponents(
350 enabledComponents: Collection<ComponentName>
351 ) {
352 sharedPrefsLock.withLock {
353 val enabledComponentsStringSet =
354 enabledComponents.map { it.flattenToShortString() }.toSet()
355 val notifiedComponents = getNotifiedComponents()
356 // Filter to only components that have enabled listeners
357 val enabledNotifiedComponents =
358 notifiedComponents.filter { enabledComponentsStringSet.contains(it) }.toSet()
359 sharedPrefs
360 .edit()
361 .putStringSet(KEY_ALREADY_NOTIFIED_COMPONENTS, enabledNotifiedComponents)
362 .apply()
363 }
364 }
365
markComponentAsNotifiednull366 suspend fun markComponentAsNotified(component: ComponentName) {
367 sharedPrefsLock.withLock {
368 val notifiedComponents = getNotifiedComponents()
369 notifiedComponents.add(component.flattenToShortString())
370 sharedPrefs
371 .edit()
372 .putStringSet(KEY_ALREADY_NOTIFIED_COMPONENTS, notifiedComponents)
373 .apply()
374 }
375 }
376
removeFromNotifiedComponentsnull377 suspend fun removeFromNotifiedComponents(packageName: String) {
378 sharedPrefsLock.withLock {
379 val notifiedComponents = getNotifiedComponents()
380 val filteredServices =
381 notifiedComponents
382 .filter {
383 val notifiedComponentName = ComponentName.unflattenFromString(it)
384 return@filter notifiedComponentName?.packageName != packageName
385 }
386 .toSet()
387 if (filteredServices.size < notifiedComponents.size) {
388 sharedPrefs
389 .edit()
390 .putStringSet(KEY_ALREADY_NOTIFIED_COMPONENTS, filteredServices)
391 .apply()
392 }
393 }
394 }
395
removeFromNotifiedComponentsnull396 suspend fun removeFromNotifiedComponents(component: ComponentName) {
397 val componentNameShortString = component.flattenToShortString()
398 sharedPrefsLock.withLock {
399 val notifiedComponents = getNotifiedComponents()
400 val componentRemoved = notifiedComponents.remove(componentNameShortString)
401 if (componentRemoved) {
402 sharedPrefs
403 .edit()
404 .putStringSet(KEY_ALREADY_NOTIFIED_COMPONENTS, notifiedComponents)
405 .apply()
406 }
407 }
408 }
409
getLastNotificationShownTimeMillisnull410 private fun getLastNotificationShownTimeMillis(): Long {
411 return sharedPrefs.getLong(KEY_LAST_NOTIFICATION_LISTENER_NOTIFICATION_SHOWN, 0)
412 }
413
updateLastShownNotificationTimenull414 private suspend fun updateLastShownNotificationTime() {
415 sharedPrefsLock.withLock {
416 sharedPrefs
417 .edit()
418 .putLong(KEY_LAST_NOTIFICATION_LISTENER_NOTIFICATION_SHOWN, currentTimeMillis())
419 .apply()
420 }
421 }
422
423 @Throws(InterruptedException::class)
postSystemNotificationIfNeedednull424 private suspend fun postSystemNotificationIfNeeded(
425 components: List<ComponentName>,
426 sessionId: Long
427 ) {
428 val componentsInternal = components.toMutableList()
429
430 // Don't show too many notification within certain timespan
431 if (
432 currentTimeMillis() - getLastNotificationShownTimeMillis() <
433 getInBetweenNotificationsMillis()
434 ) {
435 if (DEBUG) {
436 Log.d(
437 TAG,
438 "Notification not posted, within " +
439 "$DEFAULT_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS ms"
440 )
441 }
442 return
443 }
444
445 // Check for existing notification first, exit if one already present
446 if (getCurrentlyShownNotificationLocked() != null) {
447 if (DEBUG) {
448 Log.d(TAG, "Notification not posted, previous notification has not been dismissed")
449 }
450 return
451 }
452
453 // Get a random package and resolve package info
454 var pkgInfo: PackageInfo? = null
455 var componentToNotifyFor: ComponentName? = null
456 while (pkgInfo == null || componentToNotifyFor == null) {
457 throwInterruptedExceptionIfTaskIsCanceled()
458
459 if (componentsInternal.isEmpty()) {
460 if (DEBUG) {
461 Log.d(TAG, "Notification not posted, no unnotified enabled listeners")
462 }
463 return
464 }
465
466 componentToNotifyFor = componentsInternal[random.nextInt(componentsInternal.size)]
467 try {
468 if (DEBUG) {
469 Log.d(
470 TAG,
471 "Attempting to get PackageInfo for " + componentToNotifyFor.packageName
472 )
473 }
474 pkgInfo =
475 Utils.getPackageInfoForComponentName(parentUserContext, componentToNotifyFor)
476 } catch (e: PackageManager.NameNotFoundException) {
477 if (DEBUG) {
478 Log.w(TAG, "${componentToNotifyFor.packageName} not found")
479 }
480 componentsInternal.remove(componentToNotifyFor)
481 }
482 }
483
484 createPermissionReminderChannel()
485 createNotificationForNotificationListener(componentToNotifyFor, pkgInfo, sessionId)
486
487 // Mark as notified, since we don't get the on-click
488 markComponentAsNotified(componentToNotifyFor)
489 }
490
491 /** Create the channel the notification listener notifications should be posted to. */
createPermissionReminderChannelnull492 private fun createPermissionReminderChannel() {
493 val permissionReminderChannel =
494 NotificationChannel(
495 Constants.PERMISSION_REMINDER_CHANNEL_ID,
496 parentUserContext.getString(R.string.permission_reminders),
497 NotificationManager.IMPORTANCE_LOW
498 )
499
500 val notificationManager =
501 getSystemServiceSafe(parentUserContext, NotificationManager::class.java)
502
503 notificationManager.createNotificationChannel(permissionReminderChannel)
504 }
505
506 /**
507 * Create a notification reminding the user that a package has an enabled notification listener.
508 * From this notification the user can directly go to Safety Center to assess issue.
509 *
510 * @param componentName the [ComponentName] of the Notification Listener
511 * @param pkg The [PackageInfo] for the [ComponentName] package
512 */
createNotificationForNotificationListenernull513 private suspend fun createNotificationForNotificationListener(
514 componentName: ComponentName,
515 pkg: PackageInfo,
516 sessionId: Long
517 ) {
518 val pkgLabel = Utils.getApplicationLabel(parentUserContext, pkg.applicationInfo!!)
519 val uid = pkg.applicationInfo!!.uid
520
521 val deletePendingIntent =
522 getNotificationDeletePendingIntent(parentUserContext, componentName, uid, sessionId)
523 val clickPendingIntent =
524 getSafetyCenterActivityPendingIntent(parentUserContext, componentName, uid, sessionId)
525
526 val title =
527 parentUserContext.getString(R.string.notification_listener_reminder_notification_title)
528 val text =
529 parentUserContext.getString(
530 R.string.notification_listener_reminder_notification_content,
531 pkgLabel
532 )
533
534 val (appLabel, smallIcon, color) =
535 KotlinUtils.getSafetyCenterNotificationResources(parentUserContext)
536
537 val b: Notification.Builder =
538 Notification.Builder(parentUserContext, Constants.PERMISSION_REMINDER_CHANNEL_ID)
539 .setLocalOnly(true)
540 .setContentTitle(title)
541 .setContentText(text)
542 // Ensure entire text can be displayed, instead of being truncated to one line
543 .setStyle(Notification.BigTextStyle().bigText(text))
544 .setSmallIcon(smallIcon)
545 .setColor(color)
546 .setAutoCancel(true)
547 .setDeleteIntent(deletePendingIntent)
548 .setContentIntent(clickPendingIntent)
549
550 if (appLabel.isNotEmpty()) {
551 val appNameExtras = Bundle()
552 appNameExtras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, appLabel)
553 b.addExtras(appNameExtras)
554 }
555
556 val notificationManager =
557 getSystemServiceSafe(parentUserContext, NotificationManager::class.java)
558 notificationManager.notify(
559 componentName.flattenToString(),
560 NOTIFICATION_LISTENER_CHECK_NOTIFICATION_ID,
561 b.build()
562 )
563
564 if (DEBUG) {
565 Log.d(
566 TAG,
567 "Notification listener check notification shown with component=" +
568 "${componentName.flattenToString()}, uid=$uid, sessionId=$sessionId"
569 )
570 }
571
572 PermissionControllerStatsLog.write(
573 PRIVACY_SIGNAL_NOTIFICATION_INTERACTION,
574 PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__NOTIFICATION_LISTENER,
575 uid,
576 PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_SHOWN,
577 sessionId
578 )
579 updateLastShownNotificationTime()
580 }
581
582 /** @return [PendingIntent] to safety center */
getNotificationDeletePendingIntentnull583 private fun getNotificationDeletePendingIntent(
584 context: Context,
585 componentName: ComponentName,
586 uid: Int,
587 sessionId: Long
588 ): PendingIntent {
589 val intent =
590 Intent(
591 parentUserContext,
592 NotificationListenerCheckNotificationDeleteHandler::class.java
593 )
594 .apply {
595 putExtra(EXTRA_COMPONENT_NAME, componentName)
596 putExtra(Constants.EXTRA_SESSION_ID, sessionId)
597 putExtra(Intent.EXTRA_UID, uid)
598 flags = FLAG_RECEIVER_FOREGROUND
599 identifier = componentName.flattenToString()
600 }
601 return PendingIntent.getBroadcast(
602 context,
603 0,
604 intent,
605 FLAG_ONE_SHOT or FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
606 )
607 }
608
609 /** @return [PendingIntent] to safety center */
getSafetyCenterActivityPendingIntentnull610 private fun getSafetyCenterActivityPendingIntent(
611 context: Context,
612 componentName: ComponentName,
613 uid: Int,
614 sessionId: Long
615 ): PendingIntent {
616 val intent =
617 Intent(Intent.ACTION_SAFETY_CENTER).apply {
618 putExtra(EXTRA_SAFETY_SOURCE_ID, SC_NLS_SOURCE_ID)
619 putExtra(
620 EXTRA_SAFETY_SOURCE_ISSUE_ID,
621 getSafetySourceIssueIdFromComponentName(componentName)
622 )
623 putExtra(EXTRA_COMPONENT_NAME, componentName)
624 putExtra(Constants.EXTRA_SESSION_ID, sessionId)
625 putExtra(Intent.EXTRA_UID, uid)
626 flags = FLAG_ACTIVITY_NEW_TASK
627 identifier = componentName.flattenToString()
628 }
629 return PendingIntent.getActivity(
630 context,
631 0,
632 intent,
633 FLAG_ONE_SHOT or FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
634 )
635 }
636
637 /**
638 * Get currently shown notification. We only ever show one notification per profile group. Also
639 * only show notifications on the parent user/profile due to NotificationManager only binding
640 * non-managed NLS.
641 *
642 * @return The notification or `null` if no notification is currently shown
643 */
getCurrentlyShownNotificationLockednull644 private fun getCurrentlyShownNotificationLocked(): StatusBarNotification? {
645 val notifications =
646 getSystemServiceSafe(parentUserContext, NotificationManager::class.java)
647 .activeNotifications
648
649 return notifications.firstOrNull { it.id == NOTIFICATION_LISTENER_CHECK_NOTIFICATION_ID }
650 }
651
652 /** Remove any posted notifications for this feature */
removeAnyNotificationnull653 internal fun removeAnyNotification() {
654 cancelNotification()
655 }
656
657 /** Remove notification if present for a package */
removeNotificationsForPackagenull658 internal fun removeNotificationsForPackage(pkg: String) {
659 val notification: StatusBarNotification = getCurrentlyShownNotificationLocked() ?: return
660 val notificationComponent = ComponentName.unflattenFromString(notification.tag)
661 if (notificationComponent == null || notificationComponent.packageName != pkg) {
662 return
663 }
664 cancelNotification(notification.tag)
665 }
666
667 /** Remove notification if present for a [ComponentName] */
removeNotificationsForComponentnull668 internal fun removeNotificationsForComponent(component: ComponentName) {
669 val notification: StatusBarNotification = getCurrentlyShownNotificationLocked() ?: return
670 val notificationComponent = ComponentName.unflattenFromString(notification.tag)
671 if (notificationComponent == null || notificationComponent != component) {
672 return
673 }
674 cancelNotification(notification.tag)
675 }
676
cancelNotificationnull677 private fun cancelNotification(notificationTag: String) {
678 getSystemServiceSafe(parentUserContext, NotificationManager::class.java)
679 .cancel(notificationTag, NOTIFICATION_LISTENER_CHECK_NOTIFICATION_ID)
680 }
681
cancelNotificationnull682 private fun cancelNotification() {
683 getSystemServiceSafe(parentUserContext, NotificationManager::class.java)
684 .cancel(NOTIFICATION_LISTENER_CHECK_NOTIFICATION_ID)
685 }
686
sendIssuesToSafetyCenternull687 internal fun sendIssuesToSafetyCenter(
688 safetyEvent: SafetyEvent = sourceStateChangedSafetyEvent
689 ) {
690 val enabledComponents = getEnabledNotificationListeners()
691 var sessionId = Constants.INVALID_SESSION_ID
692 while (sessionId == Constants.INVALID_SESSION_ID) {
693 sessionId = random.nextLong()
694 }
695 sendIssuesToSafetyCenter(enabledComponents, sessionId, safetyEvent)
696 }
697
sendIssuesToSafetyCenternull698 private fun sendIssuesToSafetyCenter(
699 enabledComponents: List<ComponentName>,
700 sessionId: Long,
701 safetyEvent: SafetyEvent = sourceStateChangedSafetyEvent
702 ) {
703 val pendingIssues = enabledComponents.mapNotNull { createSafetySourceIssue(it, sessionId) }
704 val dataBuilder = SafetySourceData.Builder()
705 pendingIssues.forEach { dataBuilder.addIssue(it) }
706 val safetySourceData = dataBuilder.build()
707 val safetyCenterManager =
708 getSystemServiceSafe(parentUserContext, SafetyCenterManager::class.java)
709 safetyCenterManager.setSafetySourceData(SC_NLS_SOURCE_ID, safetySourceData, safetyEvent)
710 }
711
712 /**
713 * @param componentName enabled [NotificationListenerService]
714 * @return safety source issue, shown as the warning card in safety center. Null if unable to
715 * create safety source issue
716 */
717 @VisibleForTesting
createSafetySourceIssuenull718 fun createSafetySourceIssue(componentName: ComponentName, sessionId: Long): SafetySourceIssue? {
719 val pkgInfo: PackageInfo
720 try {
721 pkgInfo = Utils.getPackageInfoForComponentName(parentUserContext, componentName)
722 } catch (e: PackageManager.NameNotFoundException) {
723 if (DEBUG) {
724 Log.w(TAG, "${componentName.packageName} not found")
725 }
726 return null
727 }
728 val pkgLabel = Utils.getApplicationLabel(parentUserContext, pkgInfo.applicationInfo!!)
729 val safetySourceIssueId = getSafetySourceIssueIdFromComponentName(componentName)
730 val uid = pkgInfo.applicationInfo!!.uid
731
732 val disableNlsPendingIntent =
733 getDisableNlsPendingIntent(
734 parentUserContext,
735 safetySourceIssueId,
736 componentName,
737 uid,
738 sessionId
739 )
740
741 val disableNlsAction =
742 SafetySourceIssue.Action.Builder(
743 SC_NLS_DISABLE_ACTION_ID,
744 parentUserContext.getString(
745 R.string.notification_listener_remove_access_button_label
746 ),
747 disableNlsPendingIntent
748 )
749 .setWillResolve(true)
750 .setSuccessMessage(
751 parentUserContext.getString(
752 R.string.notification_listener_remove_access_success_label
753 )
754 )
755 .build()
756
757 val notificationListenerDetailSettingsPendingIntent =
758 getNotificationListenerDetailSettingsPendingIntent(
759 parentUserContext,
760 componentName,
761 uid,
762 sessionId
763 )
764
765 val showNotificationListenerSettingsAction =
766 SafetySourceIssue.Action.Builder(
767 SC_SHOW_NLS_SETTINGS_ACTION_ID,
768 parentUserContext.getString(
769 R.string.notification_listener_review_app_button_label
770 ),
771 notificationListenerDetailSettingsPendingIntent
772 )
773 .build()
774
775 val actionCardDismissPendingIntent =
776 getActionCardDismissalPendingIntent(parentUserContext, componentName, uid, sessionId)
777
778 val title =
779 parentUserContext.getString(R.string.notification_listener_reminder_notification_title)
780 val summary =
781 parentUserContext.getString(R.string.notification_listener_warning_card_content)
782
783 return SafetySourceIssue.Builder(
784 safetySourceIssueId,
785 title,
786 summary,
787 SafetySourceData.SEVERITY_LEVEL_INFORMATION,
788 SC_NLS_ISSUE_TYPE_ID
789 )
790 .setSubtitle(pkgLabel)
791 .addAction(disableNlsAction)
792 .addAction(showNotificationListenerSettingsAction)
793 .setOnDismissPendingIntent(actionCardDismissPendingIntent)
794 .setIssueCategory(SafetySourceIssue.ISSUE_CATEGORY_DEVICE)
795 .build()
796 }
797
798 /** @return [PendingIntent] for remove access button on the warning card. */
getDisableNlsPendingIntentnull799 private fun getDisableNlsPendingIntent(
800 context: Context,
801 safetySourceIssueId: String,
802 componentName: ComponentName,
803 uid: Int,
804 sessionId: Long
805 ): PendingIntent {
806 val intent =
807 Intent(context, DisableNotificationListenerComponentHandler::class.java).apply {
808 putExtra(EXTRA_SAFETY_SOURCE_ISSUE_ID, safetySourceIssueId)
809 putExtra(EXTRA_COMPONENT_NAME, componentName)
810 putExtra(Constants.EXTRA_SESSION_ID, sessionId)
811 putExtra(Intent.EXTRA_UID, uid)
812 flags = FLAG_RECEIVER_FOREGROUND
813 identifier = componentName.flattenToString()
814 }
815
816 return PendingIntent.getBroadcast(context, 0, intent, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
817 }
818
819 /** @return [PendingIntent] to Notification Listener Detail Settings page */
getNotificationListenerDetailSettingsPendingIntentnull820 private fun getNotificationListenerDetailSettingsPendingIntent(
821 context: Context,
822 componentName: ComponentName,
823 uid: Int,
824 sessionId: Long
825 ): PendingIntent {
826 val intent =
827 Intent(ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS).apply {
828 flags = FLAG_ACTIVITY_NEW_TASK
829 identifier = componentName.flattenToString()
830 putExtra(
831 EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME,
832 componentName.flattenToString()
833 )
834 putExtra(Constants.EXTRA_SESSION_ID, sessionId)
835 putExtra(Intent.EXTRA_UID, uid)
836 putExtra(Constants.EXTRA_IS_FROM_SLICE, true)
837 }
838 return PendingIntent.getActivity(context, 0, intent, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
839 }
840
getActionCardDismissalPendingIntentnull841 private fun getActionCardDismissalPendingIntent(
842 context: Context,
843 componentName: ComponentName,
844 uid: Int,
845 sessionId: Long
846 ): PendingIntent {
847 val intent =
848 Intent(context, NotificationListenerActionCardDismissalReceiver::class.java).apply {
849 putExtra(EXTRA_COMPONENT_NAME, componentName)
850 putExtra(Constants.EXTRA_SESSION_ID, sessionId)
851 putExtra(Intent.EXTRA_UID, uid)
852 flags = FLAG_RECEIVER_FOREGROUND
853 identifier = componentName.flattenToString()
854 }
855 return PendingIntent.getBroadcast(context, 0, intent, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
856 }
857
858 /** If [.shouldCancel] throw an [InterruptedException]. */
859 @Throws(InterruptedException::class)
throwInterruptedExceptionIfTaskIsCancelednull860 private fun throwInterruptedExceptionIfTaskIsCanceled() {
861 if (shouldCancel != null && shouldCancel.asBoolean) {
862 throw InterruptedException()
863 }
864 }
865 }
866
867 /** Checks if a new notification should be shown. */
868 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
869 class NotificationListenerCheckJobService : JobService() {
870 private var notificationListenerCheckInternal: NotificationListenerCheckInternal? = null
871 private val jobLock = Object()
872
873 /** We currently check if we should show a notification, the task executing the check */
874 @GuardedBy("jobLock") private var addNotificationListenerNotificationIfNeededJob: Job? = null
875
onCreatenull876 override fun onCreate() {
877 super.onCreate()
878 if (DEBUG) Log.d(TAG, "Nls privacy job created")
879 if (!checkNotificationListenerCheckEnabled(this)) {
880 // NotificationListenerCheck not enabled. End job.
881 return
882 }
883
884 notificationListenerCheckInternal =
885 NotificationListenerCheckInternal(
886 this,
887 BooleanSupplier {
888 synchronized(jobLock) {
889 val job = addNotificationListenerNotificationIfNeededJob
890 return@BooleanSupplier job?.isCancelled ?: false
891 }
892 }
893 )
894 }
895
896 /**
897 * Starts an asynchronous check if a notification listener notification should be shown.
898 *
899 * @param params Not used other than for interacting with job scheduling
900 * @return `false` if another check is already running, or if SDK Check fails (below T)
901 */
onStartJobnull902 override fun onStartJob(params: JobParameters): Boolean {
903 if (DEBUG) Log.d(TAG, "Nls privacy job started")
904 if (!checkNotificationListenerCheckEnabled(this)) {
905 // NotificationListenerCheck not enabled. End job.
906 return false
907 }
908
909 synchronized(jobLock) {
910 if (addNotificationListenerNotificationIfNeededJob != null) {
911 if (DEBUG) Log.d(TAG, "Job already running")
912 return false
913 }
914 addNotificationListenerNotificationIfNeededJob =
915 GlobalScope.launch(Default) {
916 notificationListenerCheckInternal
917 ?.getEnabledNotificationListenersAndNotifyIfNeeded(
918 params,
919 this@NotificationListenerCheckJobService
920 )
921 ?: jobFinished(params, true)
922 }
923 }
924 return true
925 }
926
927 /**
928 * Abort the check if still running.
929 *
930 * @param params ignored
931 * @return false
932 */
onStopJobnull933 override fun onStopJob(params: JobParameters): Boolean {
934 var job: Job?
935 synchronized(jobLock) {
936 job =
937 if (addNotificationListenerNotificationIfNeededJob == null) {
938 return false
939 } else {
940 addNotificationListenerNotificationIfNeededJob
941 }
942 }
943 job?.cancel()
944 return false
945 }
946
clearJobnull947 fun clearJob() {
948 synchronized(jobLock) { addNotificationListenerNotificationIfNeededJob = null }
949 }
950 }
951
952 /** On boot set up a periodic job that starts checks. */
953 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
954 class SetupPeriodicNotificationListenerCheck : BroadcastReceiver() {
955
onReceivenull956 override fun onReceive(context: Context, intent: Intent) {
957 if (!checkNotificationListenerCheckSupported()) {
958 // Notification Listener Check not supported. Exit.
959 return
960 }
961
962 if (isProfile(context)) {
963 // Profile parent handles child profiles too.
964 return
965 }
966
967 val jobScheduler = getSystemServiceSafe(context, JobScheduler::class.java)
968 if (jobScheduler.getPendingJob(PERIODIC_NOTIFICATION_LISTENER_CHECK_JOB_ID) == null) {
969 val job =
970 JobInfo.Builder(
971 PERIODIC_NOTIFICATION_LISTENER_CHECK_JOB_ID,
972 ComponentName(context, NotificationListenerCheckJobService::class.java)
973 )
974 .setPeriodic(getPeriodicCheckIntervalMillis(), getFlexForPeriodicCheckMillis())
975 .build()
976 val scheduleResult = jobScheduler.schedule(job)
977 if (scheduleResult != JobScheduler.RESULT_SUCCESS) {
978 Log.e(
979 TAG,
980 "Could not schedule periodic notification listener check $scheduleResult"
981 )
982 } else if (DEBUG) {
983 Log.i(TAG, "Scheduled periodic notification listener check")
984 }
985 }
986 }
987 }
988
989 /** Handle the case where the notification is swiped away without further interaction. */
990 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
991 class NotificationListenerCheckNotificationDeleteHandler : BroadcastReceiver() {
onReceivenull992 override fun onReceive(context: Context, intent: Intent) {
993 if (!checkNotificationListenerCheckSupported()) {
994 return
995 }
996
997 val componentName =
998 Utils.getParcelableExtraSafe<ComponentName>(intent, EXTRA_COMPONENT_NAME)
999 val sessionId =
1000 intent.getLongExtra(Constants.EXTRA_SESSION_ID, Constants.INVALID_SESSION_ID)
1001 val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
1002
1003 GlobalScope.launch(Default) {
1004 NotificationListenerCheckInternal(context, null).markComponentAsNotified(componentName)
1005 }
1006 if (DEBUG) {
1007 Log.d(
1008 TAG,
1009 "Notification listener check notification declined with component=" +
1010 "${componentName.flattenToString()} , uid=$uid, sessionId=$sessionId"
1011 )
1012 }
1013 PermissionControllerStatsLog.write(
1014 PRIVACY_SIGNAL_NOTIFICATION_INTERACTION,
1015 PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__NOTIFICATION_LISTENER,
1016 uid,
1017 PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__DISMISSED,
1018 sessionId
1019 )
1020 }
1021 }
1022
1023 /** Disable a specified Notification Listener Service component */
1024 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
1025 class DisableNotificationListenerComponentHandler : BroadcastReceiver() {
onReceivenull1026 override fun onReceive(context: Context, intent: Intent) {
1027 if (DEBUG) Log.d(TAG, "DisableComponentHandler.onReceive $intent")
1028 val componentName =
1029 Utils.getParcelableExtraSafe<ComponentName>(intent, EXTRA_COMPONENT_NAME)
1030 val sessionId =
1031 intent.getLongExtra(Constants.EXTRA_SESSION_ID, Constants.INVALID_SESSION_ID)
1032 val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
1033
1034 GlobalScope.launch(Default) {
1035 if (DEBUG) {
1036 Log.d(
1037 TAG,
1038 "DisableComponentHandler: disabling $componentName," +
1039 "uid=$uid, sessionId=$sessionId"
1040 )
1041 }
1042
1043 val safetyEventBuilder =
1044 try {
1045 val notificationManager =
1046 getSystemServiceSafe(context, NotificationManager::class.java)
1047 disallowNlsLock.withLock {
1048 notificationManager.setNotificationListenerAccessGranted(
1049 componentName,
1050 /* granted= */ false,
1051 /* userSet= */ true
1052 )
1053 }
1054
1055 SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED)
1056 } catch (e: Exception) {
1057 Log.w(TAG, "error occurred in disabling notification listener service.", e)
1058 SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED)
1059 }
1060
1061 val safetySourceIssueId: String? = intent.getStringExtra(EXTRA_SAFETY_SOURCE_ISSUE_ID)
1062 val safetyEvent =
1063 safetyEventBuilder
1064 .setSafetySourceIssueId(safetySourceIssueId)
1065 .setSafetySourceIssueActionId(SC_NLS_DISABLE_ACTION_ID)
1066 .build()
1067
1068 NotificationListenerCheckInternal(context, null).run {
1069 removeNotificationsForComponent(componentName)
1070 removeFromNotifiedComponents(componentName)
1071 sendIssuesToSafetyCenter(safetyEvent)
1072 }
1073 PermissionControllerStatsLog.write(
1074 PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION,
1075 PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__NOTIFICATION_LISTENER,
1076 uid,
1077 PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CLICKED_CTA1,
1078 sessionId
1079 )
1080 }
1081 }
1082
1083 companion object {
1084 private val disallowNlsLock = Mutex()
1085 }
1086 }
1087
1088 /* A Safety Center action card for a specified component was dismissed */
1089 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
1090 class NotificationListenerActionCardDismissalReceiver : BroadcastReceiver() {
onReceivenull1091 override fun onReceive(context: Context, intent: Intent) {
1092 if (DEBUG) Log.d(TAG, "ActionCardDismissalReceiver.onReceive $intent")
1093 val componentName =
1094 Utils.getParcelableExtraSafe<ComponentName>(intent, EXTRA_COMPONENT_NAME)
1095 val sessionId =
1096 intent.getLongExtra(Constants.EXTRA_SESSION_ID, Constants.INVALID_SESSION_ID)
1097 val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
1098
1099 GlobalScope.launch(Default) {
1100 if (DEBUG) {
1101 Log.d(
1102 TAG,
1103 "ActionCardDismissalReceiver: $componentName dismissed," +
1104 "uid=$uid, sessionId=$sessionId"
1105 )
1106 }
1107 NotificationListenerCheckInternal(context, null).run {
1108 removeNotificationsForComponent(componentName)
1109 markComponentAsNotified(componentName)
1110 }
1111 }
1112 PermissionControllerStatsLog.write(
1113 PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION,
1114 PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__NOTIFICATION_LISTENER,
1115 uid,
1116 PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CARD_DISMISSED,
1117 sessionId
1118 )
1119 }
1120 }
1121
1122 /**
1123 * If a package gets removed or the data of the package gets cleared, forget that we showed a
1124 * notification for it.
1125 */
1126 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
1127 class NotificationListenerPackageResetHandler : BroadcastReceiver() {
onReceivenull1128 override fun onReceive(context: Context, intent: Intent) {
1129 val action = intent.action
1130 if (
1131 action != Intent.ACTION_PACKAGE_DATA_CLEARED &&
1132 action != Intent.ACTION_PACKAGE_FULLY_REMOVED
1133 ) {
1134 return
1135 }
1136
1137 if (!checkNotificationListenerCheckEnabled(context)) {
1138 return
1139 }
1140
1141 if (isProfile(context)) {
1142 if (DEBUG) {
1143 Log.d(TAG, "NotificationListenerCheck only supports parent profile")
1144 }
1145 return
1146 }
1147
1148 val data = requireNotNull(intent.data)
1149 val pkg: String = data.schemeSpecificPart
1150
1151 if (DEBUG) Log.i(TAG, "Reset $pkg")
1152
1153 GlobalScope.launch(Default) {
1154 NotificationListenerCheckInternal(context, null).run {
1155 removeNotificationsForPackage(pkg)
1156 removeFromNotifiedComponents(pkg)
1157 sendIssuesToSafetyCenter()
1158 }
1159 }
1160 }
1161 }
1162
1163 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
1164 class NotificationListenerPrivacySource : PrivacySource {
1165 override val shouldProcessProfileRequest: Boolean = false
1166
safetyCenterEnabledChangednull1167 override fun safetyCenterEnabledChanged(context: Context, enabled: Boolean) {
1168 NotificationListenerCheckInternal(context, null).run { removeAnyNotification() }
1169 }
1170
rescanAndPushSafetyCenterDatanull1171 override fun rescanAndPushSafetyCenterData(
1172 context: Context,
1173 intent: Intent,
1174 refreshEvent: RefreshEvent
1175 ) {
1176 if (!isNotificationListenerCheckFlagEnabled()) {
1177 return
1178 }
1179
1180 val safetyRefreshEvent = getSafetyCenterEvent(refreshEvent, intent)
1181
1182 NotificationListenerCheckInternal(context, null).run {
1183 sendIssuesToSafetyCenter(safetyRefreshEvent)
1184 }
1185 }
1186 }
1187