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