• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.accessibilityservice.AccessibilityServiceInfo
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.SharedPreferences
33 import android.os.Build
34 import android.os.Bundle
35 import android.provider.DeviceConfig
36 import android.provider.Settings
37 import android.safetycenter.SafetyCenterManager
38 import android.safetycenter.SafetyEvent
39 import android.safetycenter.SafetySourceData
40 import android.safetycenter.SafetySourceIssue
41 import android.service.notification.StatusBarNotification
42 import android.util.Log
43 import android.view.accessibility.AccessibilityManager
44 import androidx.annotation.ChecksSdkIntAtLeast
45 import androidx.annotation.GuardedBy
46 import androidx.annotation.RequiresApi
47 import androidx.annotation.VisibleForTesting
48 import androidx.annotation.WorkerThread
49 import androidx.core.util.Preconditions
50 import com.android.modules.utils.build.SdkLevel
51 import com.android.permissioncontroller.Constants
52 import com.android.permissioncontroller.PermissionControllerStatsLog
53 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION
54 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CARD_DISMISSED
55 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CLICKED_CTA1
56 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__A11Y_SERVICE
57 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION
58 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__DISMISSED
59 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_SHOWN
60 import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__A11Y_SERVICE
61 import com.android.permissioncontroller.R
62 import com.android.permissioncontroller.permission.utils.KotlinUtils
63 import com.android.permissioncontroller.permission.utils.Utils
64 import com.android.permissioncontroller.permission.utils.Utils.getSystemServiceSafe
65 import com.android.permissioncontroller.privacysources.SafetyCenterReceiver.RefreshEvent
66 import java.util.Random
67 import java.util.concurrent.TimeUnit
68 import java.util.function.BooleanSupplier
69 import kotlinx.coroutines.CoroutineScope
70 import kotlinx.coroutines.Dispatchers
71 import kotlinx.coroutines.Job
72 import kotlinx.coroutines.SupervisorJob
73 import kotlinx.coroutines.launch
74 import kotlinx.coroutines.sync.Mutex
75 import kotlinx.coroutines.sync.withLock
76 
77 @VisibleForTesting
78 const val PROPERTY_SC_ACCESSIBILITY_SOURCE_ENABLED = "sc_accessibility_source_enabled"
79 const val PROPERTY_SC_ACCESSIBILITY_LISTENER_ENABLED = "sc_accessibility_listener_enabled"
80 const val SC_ACCESSIBILITY_SOURCE_ID = "AndroidAccessibility"
81 const val SC_ACCESSIBILITY_REMOVE_ACCESS_ACTION_ID =
82     "revoke_accessibility_app_access"
83 private const val DEBUG = false
84 
85 @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
86 private fun isAccessibilitySourceSupported(): Boolean {
87     return SdkLevel.isAtLeastT()
88 }
89 
isAccessibilitySourceEnablednull90 fun isAccessibilitySourceEnabled(): Boolean {
91     return DeviceConfig.getBoolean(
92         DeviceConfig.NAMESPACE_PRIVACY,
93         PROPERTY_SC_ACCESSIBILITY_SOURCE_ENABLED,
94         true
95     )
96 }
97 
98 /**
99  * cts test needs to disable the listener.
100  */
isAccessibilityListenerEnablednull101 fun isAccessibilityListenerEnabled(): Boolean {
102     return DeviceConfig.getBoolean(
103         DeviceConfig.NAMESPACE_PRIVACY,
104         PROPERTY_SC_ACCESSIBILITY_LISTENER_ENABLED,
105         true
106     )
107 }
108 
109 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
isSafetyCenterEnablednull110 private fun isSafetyCenterEnabled(context: Context): Boolean {
111     return getSystemServiceSafe(context, SafetyCenterManager::class.java)
112         .isSafetyCenterEnabled
113 }
114 
115 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
116 class AccessibilitySourceService(
117     val context: Context,
118     val random: Random = Random()
119 ) : PrivacySource {
120 
121     private val parentUserContext = Utils.getParentUserContext(context)
122     private val packageManager = parentUserContext.packageManager
123     private val sharedPrefs: SharedPreferences = parentUserContext.getSharedPreferences(
124         ACCESSIBILITY_PREFERENCES_FILE, Context.MODE_PRIVATE)
125     private val notificationsManager = getSystemServiceSafe(parentUserContext,
126         NotificationManager::class.java)
127     private val safetyCenterManager = getSystemServiceSafe(parentUserContext,
128         SafetyCenterManager::class.java)
129 
130     @WorkerThread
processAccessibilityJobnull131     internal suspend fun processAccessibilityJob(
132         params: JobParameters?,
133         jobService: AccessibilityJobService,
134         cancel: BooleanSupplier?
135     ) {
136         lock.withLock {
137             try {
138                 var sessionId = Constants.INVALID_SESSION_ID
139                 while (sessionId == Constants.INVALID_SESSION_ID) {
140                     sessionId = random.nextLong()
141                 }
142                 if (DEBUG) {
143                     Log.v(LOG_TAG, "safety center accessibility privacy job started.")
144                 }
145                 interruptJobIfCanceled(cancel)
146                 val a11yServiceList = getEnabledAccessibilityServices()
147                 if (a11yServiceList.isEmpty()) {
148                     if (DEBUG) {
149                         Log.v(LOG_TAG, "accessibility services not enabled, job completed.")
150                     }
151                     jobService.jobFinished(params, false)
152                     jobService.clearJob()
153                     return
154                 }
155 
156                 val lastShownNotification =
157                     sharedPrefs.getLong(KEY_LAST_ACCESSIBILITY_NOTIFICATION_SHOWN, 0)
158                 val showNotification = ((System.currentTimeMillis() - lastShownNotification) >
159                     getNotificationsIntervalMillis()) && getCurrentNotification() == null
160 
161                 if (showNotification) {
162                     val alreadyNotifiedServices = getNotifiedServices()
163 
164                     val toBeNotifiedServices = a11yServiceList.filter {
165                         !alreadyNotifiedServices.contains(it.id)
166                     }
167 
168                     if (toBeNotifiedServices.isNotEmpty()) {
169                         if (DEBUG) {
170                             Log.v(LOG_TAG, "sending an accessibility service notification")
171                         }
172                         val serviceToBeNotified: AccessibilityServiceInfo =
173                             toBeNotifiedServices[random.nextInt(toBeNotifiedServices.size)]
174                         createPermissionReminderChannel()
175                         interruptJobIfCanceled(cancel)
176                         sendNotification(serviceToBeNotified, sessionId)
177                     }
178                 }
179 
180                 interruptJobIfCanceled(cancel)
181                 sendIssuesToSafetyCenter(a11yServiceList, sessionId)
182                 jobService.jobFinished(params, false)
183             } catch (ex: InterruptedException) {
184                 Log.w(LOG_TAG, "cancel request for safety center accessibility job received.")
185                 jobService.jobFinished(params, true)
186             } catch (ex: Exception) {
187                 Log.w(LOG_TAG, "could not process safety center accessibility job", ex)
188                 jobService.jobFinished(params, false)
189             } finally {
190                 jobService.clearJob()
191             }
192         }
193     }
194 
195     /**
196      * sends a notification for a given accessibility package
197      */
sendNotificationnull198     private suspend fun sendNotification(
199         serviceToBeNotified: AccessibilityServiceInfo,
200         sessionId: Long
201     ) {
202         val pkgLabel = serviceToBeNotified.resolveInfo.loadLabel(packageManager)
203         val componentName = ComponentName.unflattenFromString(serviceToBeNotified.id)!!
204         val uid = serviceToBeNotified.resolveInfo.serviceInfo.applicationInfo.uid
205 
206         val notificationDeleteIntent =
207             Intent(parentUserContext, AccessibilityNotificationDeleteHandler::class.java).apply {
208                 putExtra(Intent.EXTRA_COMPONENT_NAME, componentName)
209                 putExtra(Constants.EXTRA_SESSION_ID, sessionId)
210                 putExtra(Intent.EXTRA_UID, uid)
211                 flags = Intent.FLAG_RECEIVER_FOREGROUND
212                 identifier = componentName.flattenToString()
213             }
214 
215         val title = parentUserContext.getString(
216             R.string.accessibility_access_reminder_notification_title
217         )
218         val summary = parentUserContext.getString(
219             R.string.accessibility_access_reminder_notification_content,
220             pkgLabel
221         )
222 
223         val (appLabel, smallIcon, color) =
224             KotlinUtils.getSafetyCenterNotificationResources(parentUserContext)
225         val b: Notification.Builder =
226             Notification.Builder(parentUserContext, Constants.PERMISSION_REMINDER_CHANNEL_ID)
227                 .setLocalOnly(true)
228                 .setContentTitle(title)
229                 .setContentText(summary)
230                 // Ensure entire text can be displayed, instead of being truncated to one line
231                 .setStyle(Notification.BigTextStyle().bigText(summary))
232                 .setSmallIcon(smallIcon)
233                 .setColor(color)
234                 .setAutoCancel(true)
235                 .setDeleteIntent(
236                     PendingIntent.getBroadcast(
237                         parentUserContext, 0, notificationDeleteIntent,
238                         PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT or
239                             PendingIntent.FLAG_IMMUTABLE
240                     )
241                 )
242                 .setContentIntent(getSafetyCenterActivityIntent(context, uid, sessionId))
243 
244         val appNameExtras = Bundle()
245         appNameExtras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, appLabel)
246         b.addExtras(appNameExtras)
247 
248         notificationsManager.notify(
249             componentName.flattenToShortString(),
250             Constants.ACCESSIBILITY_CHECK_NOTIFICATION_ID,
251             b.build()
252         )
253 
254         sharedPrefsLock.withLock {
255             sharedPrefs.edit().putLong(
256                 KEY_LAST_ACCESSIBILITY_NOTIFICATION_SHOWN,
257                 System.currentTimeMillis()
258             ).apply()
259         }
260         markServiceAsNotified(ComponentName.unflattenFromString(serviceToBeNotified.id)!!)
261 
262         if (DEBUG) {
263             Log.v(LOG_TAG, "NOTIF_INTERACTION SEND metric, uid $uid session $sessionId")
264         }
265         PermissionControllerStatsLog.write(
266             PRIVACY_SIGNAL_NOTIFICATION_INTERACTION,
267             PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__A11Y_SERVICE,
268             uid,
269             PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_SHOWN,
270             sessionId
271         )
272     }
273 
274     /** Create the channel for a11y notifications */
createPermissionReminderChannelnull275     private fun createPermissionReminderChannel() {
276         val permissionReminderChannel = NotificationChannel(
277             Constants.PERMISSION_REMINDER_CHANNEL_ID,
278             context.getString(R.string.permission_reminders),
279             NotificationManager.IMPORTANCE_LOW
280         )
281         notificationsManager.createNotificationChannel(permissionReminderChannel)
282     }
283 
284     /**
285      * @param a11yService enabled 3rd party accessibility service
286      * @return safety source issue, shown as the warning card in safety center
287      */
createSafetySourceIssuenull288     private fun createSafetySourceIssue(
289         a11yService: AccessibilityServiceInfo,
290         sessionId: Long
291     ): SafetySourceIssue {
292         val componentName = ComponentName.unflattenFromString(a11yService.id)!!
293         val safetySourceIssueId = "accessibility_${componentName.flattenToString()}"
294         val pkgLabel = a11yService.resolveInfo.loadLabel(packageManager).toString()
295         val uid = a11yService.resolveInfo.serviceInfo.applicationInfo.uid
296 
297         val removeAccessPendingIntent = getRemoveAccessPendingIntent(
298             context,
299             componentName,
300             safetySourceIssueId,
301             uid,
302             sessionId
303         )
304 
305         val removeAccessAction = SafetySourceIssue.Action.Builder(
306             SC_ACCESSIBILITY_REMOVE_ACCESS_ACTION_ID,
307             parentUserContext.getString(R.string.accessibility_remove_access_button_label),
308             removeAccessPendingIntent
309         )
310             .setWillResolve(true)
311             .setSuccessMessage(parentUserContext.getString(
312                 R.string.accessibility_remove_access_success_label))
313             .build()
314 
315         val accessibilityActivityPendingIntent =
316             getAccessibilityActivityPendingIntent(context, uid, sessionId)
317 
318         val accessibilityActivityAction = SafetySourceIssue.Action.Builder(
319             SC_ACCESSIBILITY_SHOW_ACCESSIBILITY_ACTIVITY_ACTION_ID,
320             parentUserContext.getString(R.string.accessibility_show_all_apps_button_label),
321             accessibilityActivityPendingIntent
322         ).build()
323 
324         val warningCardDismissIntent =
325             Intent(parentUserContext, AccessibilityWarningCardDismissalReceiver::class.java).apply {
326                 flags = Intent.FLAG_RECEIVER_FOREGROUND
327                 identifier = componentName.flattenToString()
328                 putExtra(Intent.EXTRA_COMPONENT_NAME, componentName)
329                 putExtra(Constants.EXTRA_SESSION_ID, sessionId)
330                 putExtra(Intent.EXTRA_UID, uid)
331             }
332 
333         val warningCardDismissPendingIntent = PendingIntent.getBroadcast(
334             parentUserContext, 0, warningCardDismissIntent,
335             PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT or
336                 PendingIntent.FLAG_IMMUTABLE
337         )
338         val title = parentUserContext.getString(
339             R.string.accessibility_access_reminder_notification_title)
340         val summary = parentUserContext.getString(
341             R.string.accessibility_access_warning_card_content)
342 
343         return SafetySourceIssue.Builder(
344                 safetySourceIssueId,
345                 title,
346                 summary,
347                 SafetySourceData.SEVERITY_LEVEL_INFORMATION,
348                 SC_ACCESSIBILITY_ISSUE_TYPE_ID
349             )
350             .addAction(removeAccessAction)
351             .addAction(accessibilityActivityAction)
352             .setSubtitle(pkgLabel)
353             .setOnDismissPendingIntent(warningCardDismissPendingIntent)
354             .setIssueCategory(SafetySourceIssue.ISSUE_CATEGORY_DEVICE)
355             .build()
356     }
357 
358     /**
359      * @return pending intent for remove access button on the warning card.
360      */
getRemoveAccessPendingIntentnull361     private fun getRemoveAccessPendingIntent(
362         context: Context,
363         serviceComponentName: ComponentName,
364         safetySourceIssueId: String,
365         uid: Int,
366         sessionId: Long
367     ): PendingIntent {
368         val intent =
369             Intent(parentUserContext, AccessibilityRemoveAccessHandler::class.java).apply {
370                 putExtra(Intent.EXTRA_COMPONENT_NAME, serviceComponentName)
371                 putExtra(SafetyCenterManager.EXTRA_SAFETY_SOURCE_ISSUE_ID, safetySourceIssueId)
372                 putExtra(Constants.EXTRA_SESSION_ID, sessionId)
373                 putExtra(Intent.EXTRA_UID, uid)
374                 flags = Intent.FLAG_RECEIVER_FOREGROUND
375                 identifier = serviceComponentName.flattenToString()
376             }
377 
378         return PendingIntent.getBroadcast(
379             context,
380             0,
381             intent,
382             PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
383         )
384     }
385 
386     /**
387      * @return pending intent for redirecting user to the accessibility page
388      */
getAccessibilityActivityPendingIntentnull389     private fun getAccessibilityActivityPendingIntent(
390         context: Context,
391         uid: Int,
392         sessionId: Long
393     ): PendingIntent {
394         val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
395         intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
396         intent.putExtra(Constants.EXTRA_SESSION_ID, sessionId)
397         intent.putExtra(Intent.EXTRA_UID, uid)
398 
399         // Start this Settings activity using the same UX that settings slices uses. This allows
400         // settings to correctly support 2-pane layout with as-best-as-possible transition
401         // animation.
402         intent.putExtra(Constants.EXTRA_IS_FROM_SLICE, true)
403         return PendingIntent.getActivity(
404             context,
405             0,
406             intent,
407             PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
408         )
409     }
410 
411     /**
412      * @return pending intent to redirect the user to safety center on notification click
413      */
getSafetyCenterActivityIntentnull414     private fun getSafetyCenterActivityIntent(
415         context: Context,
416         uid: Int,
417         sessionId: Long
418     ): PendingIntent {
419         val intent = Intent(Intent.ACTION_SAFETY_CENTER)
420         intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
421         intent.putExtra(Constants.EXTRA_SESSION_ID, sessionId)
422         intent.putExtra(Intent.EXTRA_UID, uid)
423         intent.putExtra(
424             Constants.EXTRA_PRIVACY_SOURCE,
425             PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__A11Y_SERVICE
426         )
427         return PendingIntent.getActivity(
428             context,
429             0,
430             intent,
431             PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
432         )
433     }
434 
sendIssuesToSafetyCenternull435     private fun sendIssuesToSafetyCenter(
436         a11yServiceList: List<AccessibilityServiceInfo>,
437         sessionId: Long,
438         safetyEvent: SafetyEvent = sourceStateChanged
439     ) {
440         val pendingIssues = a11yServiceList.map { createSafetySourceIssue(it, sessionId) }
441         val dataBuilder = SafetySourceData.Builder()
442         pendingIssues.forEach { dataBuilder.addIssue(it) }
443         val safetySourceData = dataBuilder.build()
444         if (DEBUG) {
445             Log.v(LOG_TAG, "sending ${pendingIssues.size} issue to sc, data: $safetySourceData")
446         }
447         safetyCenterManager.setSafetySourceData(
448             SC_ACCESSIBILITY_SOURCE_ID,
449             safetySourceData,
450             safetyEvent
451         )
452     }
453 
sendIssuesToSafetyCenternull454     fun sendIssuesToSafetyCenter(
455         a11yServiceList: List<AccessibilityServiceInfo>,
456         safetyEvent: SafetyEvent = sourceStateChanged
457     ) {
458         var sessionId = Constants.INVALID_SESSION_ID
459         while (sessionId == Constants.INVALID_SESSION_ID) {
460             sessionId = random.nextLong()
461         }
462         sendIssuesToSafetyCenter(a11yServiceList, sessionId, safetyEvent)
463     }
464 
sendIssuesToSafetyCenternull465     private fun sendIssuesToSafetyCenter(safetyEvent: SafetyEvent = sourceStateChanged) {
466         val enabledServices = getEnabledAccessibilityServices()
467         sendIssuesToSafetyCenter(enabledServices, safetyEvent)
468     }
469 
470     /**
471      * If [.cancel] throw an [InterruptedException].
472      */
473     @Throws(InterruptedException::class)
interruptJobIfCancelednull474     private fun interruptJobIfCanceled(cancel: BooleanSupplier?) {
475         if (cancel != null && cancel.asBoolean) {
476             throw InterruptedException()
477         }
478     }
479 
480     private val accessibilityManager = getSystemServiceSafe(parentUserContext,
481         AccessibilityManager::class.java)
482 
483     /**
484      * @return enabled 3rd party accessibility services.
485      */
getEnabledAccessibilityServicesnull486     fun getEnabledAccessibilityServices(): List<AccessibilityServiceInfo> {
487         val installedServices = accessibilityManager.getInstalledAccessibilityServiceList()
488             .associateBy { ComponentName.unflattenFromString(it.id) }
489         val enabledServices = AccessibilitySettingsUtil.getEnabledServicesFromSettings(context)
490             .map {
491                 if (installedServices[it] == null) {
492                     Log.e(LOG_TAG, "enabled accessibility service ($it) not found in installed" +
493                         "services: ${installedServices.keys}")
494                 }
495                 installedServices[it]
496             }
497 
498         return enabledServices.filterNotNull()
499             .filter { !it.isAccessibilityTool }
500     }
501 
502     /**
503      * Get currently shown accessibility notification.
504      *
505      * @return The notification or `null` if no notification is currently shown
506      */
getCurrentNotificationnull507     private fun getCurrentNotification(): StatusBarNotification? {
508         val notifications = notificationsManager.activeNotifications
509         return notifications.firstOrNull { it.id == Constants.ACCESSIBILITY_CHECK_NOTIFICATION_ID }
510     }
511 
removeFromNotifiedServicesnull512     internal suspend fun removeFromNotifiedServices(a11Service: ComponentName) {
513         sharedPrefsLock.withLock {
514             val notifiedServices = getNotifiedServices()
515             val filteredServices = notifiedServices.filter {
516                 it != a11Service.flattenToShortString()
517             }.toSet()
518 
519             if (filteredServices.size < notifiedServices.size) {
520             sharedPrefs.edit().putStringSet(KEY_ALREADY_NOTIFIED_SERVICES, filteredServices)
521                 .apply()
522             }
523         }
524     }
525 
markServiceAsNotifiednull526     internal suspend fun markServiceAsNotified(a11Service: ComponentName) {
527         sharedPrefsLock.withLock {
528             val alreadyNotifiedServices = getNotifiedServices()
529             alreadyNotifiedServices.add(a11Service.flattenToShortString())
530             sharedPrefs.edit().putStringSet(KEY_ALREADY_NOTIFIED_SERVICES, alreadyNotifiedServices)
531                 .apply()
532         }
533     }
534 
updateServiceAsNotifiednull535     internal suspend fun updateServiceAsNotified(enabledA11yServices: Set<String>) {
536         sharedPrefsLock.withLock {
537             val alreadyNotifiedServices = getNotifiedServices()
538             val services = alreadyNotifiedServices.filter { enabledA11yServices.contains(it) }
539             if (services.size < alreadyNotifiedServices.size) {
540                 sharedPrefs.edit().putStringSet(KEY_ALREADY_NOTIFIED_SERVICES, services.toSet())
541                     .apply()
542             }
543         }
544     }
545 
getNotifiedServicesnull546     private fun getNotifiedServices(): MutableSet<String> {
547         return sharedPrefs.getStringSet(KEY_ALREADY_NOTIFIED_SERVICES, mutableSetOf<String>())!!
548     }
549 
550     @VisibleForTesting
getSharedPreferencenull551     internal fun getSharedPreference(): SharedPreferences {
552         return sharedPrefs
553     }
554 
555     /**
556      * Remove notification when safety center feature is turned off
557      */
removeAccessibilityNotificationnull558     private fun removeAccessibilityNotification() {
559         val notification: StatusBarNotification = getCurrentNotification() ?: return
560         cancelNotification(notification.tag)
561     }
562 
563     /**
564      * Remove notification (if needed) when an accessibility event occur.
565      */
removeAccessibilityNotificationnull566     fun removeAccessibilityNotification(a11yEnabledComponents: Set<String>) {
567         val notification = getCurrentNotification() ?: return
568         if (a11yEnabledComponents.contains(notification.tag)) {
569             return
570         }
571         cancelNotification(notification.tag)
572     }
573 
574     /**
575      * Remove notification when a package is uninstalled.
576      */
removeAccessibilityNotificationnull577     private fun removeAccessibilityNotification(pkg: String) {
578         val notification = getCurrentNotification() ?: return
579         val component = ComponentName.unflattenFromString(notification.tag)
580         if (component == null || component.packageName != pkg) {
581             return
582         }
583         cancelNotification(notification.tag)
584     }
585 
586     /**
587      * Remove notification for a component, when warning card is dismissed.
588      */
removeAccessibilityNotificationnull589     fun removeAccessibilityNotification(component: ComponentName) {
590         val notification = getCurrentNotification() ?: return
591         if (component.flattenToShortString() == notification.tag) {
592             cancelNotification(notification.tag)
593         }
594     }
595 
cancelNotificationnull596     private fun cancelNotification(notificationTag: String) {
597         getSystemServiceSafe(parentUserContext, NotificationManager::class.java)
598             .cancel(notificationTag, Constants.ACCESSIBILITY_CHECK_NOTIFICATION_ID)
599     }
600 
removePackageStatenull601     internal suspend fun removePackageState(pkg: String) {
602         sharedPrefsLock.withLock {
603             removeAccessibilityNotification(pkg)
604             val notifiedServices = getNotifiedServices().mapNotNull {
605                 ComponentName.unflattenFromString(it)
606             }
607 
608             val filteredServices = notifiedServices.filterNot { it.packageName == pkg }
609                 .map { it.flattenToShortString() }.toSet()
610             if (filteredServices.size < notifiedServices.size) {
611                 sharedPrefs.edit().putStringSet(
612                     KEY_ALREADY_NOTIFIED_SERVICES,
613                     filteredServices
614                 ).apply()
615             }
616         }
617     }
618 
619     companion object {
620         private val LOG_TAG = AccessibilitySourceService::class.java.simpleName
621         private const val SC_ACCESSIBILITY_ISSUE_TYPE_ID = "accessibility_privacy_issue"
622         private const val KEY_LAST_ACCESSIBILITY_NOTIFICATION_SHOWN =
623             "last_accessibility_notification_shown"
624         const val KEY_ALREADY_NOTIFIED_SERVICES = "already_notified_a11y_services"
625         private const val ACCESSIBILITY_PREFERENCES_FILE = "a11y_preferences"
626         private const val SC_ACCESSIBILITY_SHOW_ACCESSIBILITY_ACTIVITY_ACTION_ID =
627             "show_accessibility_apps"
628         private const val PROPERTY_SC_ACCESSIBILITY_JOB_INTERVAL_MILLIS =
629             "sc_accessibility_job_interval_millis"
630         private val DEFAULT_SC_ACCESSIBILITY_JOB_INTERVAL_MILLIS = TimeUnit.DAYS.toMillis(1)
631 
632         private val sourceStateChanged = SafetyEvent.Builder(
633             SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED).build()
634 
635         /** lock for processing a job */
636         internal val lock = Mutex()
637 
638         /** lock for shared preferences writes */
639         private val sharedPrefsLock = Mutex()
640 
641         /**
642          * Get time in between two periodic checks.
643          *
644          * Default: 1 day
645          *
646          * @return The time in between check in milliseconds
647          */
getJobsIntervalMillisnull648         fun getJobsIntervalMillis(): Long {
649             return DeviceConfig.getLong(
650                 DeviceConfig.NAMESPACE_PRIVACY,
651                 PROPERTY_SC_ACCESSIBILITY_JOB_INTERVAL_MILLIS,
652                 DEFAULT_SC_ACCESSIBILITY_JOB_INTERVAL_MILLIS
653             )
654         }
655 
656         /**
657          * Flexibility of the periodic check.
658          *
659          *
660          * 10% of [.getPeriodicCheckIntervalMillis]
661          *
662          * @return The flexibility of the periodic check in milliseconds
663          */
getFlexJobsIntervalMillisnull664         fun getFlexJobsIntervalMillis(): Long {
665             return getJobsIntervalMillis() / 10
666         }
667 
668         /**
669          * Minimum time in between showing two notifications.
670          *
671          *
672          * This is just small enough so that the periodic check can always show a notification.
673          *
674          * @return The minimum time in milliseconds
675          */
getNotificationsIntervalMillisnull676         private fun getNotificationsIntervalMillis(): Long {
677             return getJobsIntervalMillis() - (getFlexJobsIntervalMillis() * 2.1).toLong()
678         }
679     }
680 
681     override val shouldProcessProfileRequest: Boolean = false
682 
safetyCenterEnabledChangednull683     override fun safetyCenterEnabledChanged(context: Context, enabled: Boolean) {
684         if (!enabled) { // safety center disabled event
685             removeAccessibilityNotification()
686         }
687     }
688 
rescanAndPushSafetyCenterDatanull689     override fun rescanAndPushSafetyCenterData(
690         context: Context,
691         intent: Intent,
692         refreshEvent: RefreshEvent
693     ) {
694         if (DEBUG) {
695             Log.v(LOG_TAG, "rescan and push event from safety center $refreshEvent")
696         }
697         val safetyCenterEvent = getSafetyCenterEvent(refreshEvent, intent)
698         sendIssuesToSafetyCenter(safetyCenterEvent)
699     }
700 }
701 
702 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
703 class AccessibilityPackageResetHandler : BroadcastReceiver() {
704     private val LOG_TAG = AccessibilityPackageResetHandler::class.java.simpleName
705 
onReceivenull706     override fun onReceive(context: Context, intent: Intent) {
707         val action = intent.action
708         if (action != Intent.ACTION_PACKAGE_DATA_CLEARED &&
709             action != Intent.ACTION_PACKAGE_FULLY_REMOVED
710         ) {
711             return
712         }
713 
714         if (!isAccessibilitySourceSupported() || isProfile(context)) {
715             return
716         }
717 
718         val data = Preconditions.checkNotNull(intent.data)
719         val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
720         coroutineScope.launch(Dispatchers.Default) {
721             if (DEBUG) {
722                 Log.v(LOG_TAG, "package reset event occurred for ${data.schemeSpecificPart}")
723             }
724             AccessibilitySourceService(context).run {
725                 removePackageState(data.schemeSpecificPart)
726             }
727         }
728     }
729 }
730 
731 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
732 class AccessibilityNotificationDeleteHandler : BroadcastReceiver() {
733     private val LOG_TAG = AccessibilityNotificationDeleteHandler::class.java.simpleName
onReceivenull734     override fun onReceive(context: Context, intent: Intent) {
735         val sessionId =
736             intent.getLongExtra(Constants.EXTRA_SESSION_ID, Constants.INVALID_SESSION_ID)
737         val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
738         val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
739         coroutineScope.launch(Dispatchers.Default) {
740             if (DEBUG) {
741                 Log.v(LOG_TAG, "NOTIF_INTERACTION DISMISSED metric, uid $uid session $sessionId")
742             }
743             PermissionControllerStatsLog.write(
744                 PRIVACY_SIGNAL_NOTIFICATION_INTERACTION,
745                 PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__A11Y_SERVICE,
746                 uid,
747                 PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__DISMISSED,
748                 sessionId
749             )
750         }
751     }
752 }
753 
754 /**
755  * Handler for Remove access action (warning cards) in safety center dashboard
756  */
757 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
758 class AccessibilityRemoveAccessHandler : BroadcastReceiver() {
759     private val LOG_TAG = AccessibilityRemoveAccessHandler::class.java.simpleName
760 
onReceivenull761     override fun onReceive(context: Context, intent: Intent) {
762         val a11yService: ComponentName =
763             Utils.getParcelableExtraSafe<ComponentName>(intent, Intent.EXTRA_COMPONENT_NAME)
764         val sessionId =
765             intent.getLongExtra(Constants.EXTRA_SESSION_ID, Constants.INVALID_SESSION_ID)
766         val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
767         val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
768         coroutineScope.launch(Dispatchers.Default) {
769             if (DEBUG) {
770                 Log.v(LOG_TAG, "disabling a11y service ${a11yService.flattenToShortString()}")
771             }
772             AccessibilitySourceService.lock.withLock {
773                 val accessibilityService = AccessibilitySourceService(context)
774                 var a11yEnabledServices = accessibilityService.getEnabledAccessibilityServices()
775                 val builder = try {
776                     AccessibilitySettingsUtil.disableAccessibilityService(context, a11yService)
777                     accessibilityService.removeFromNotifiedServices(a11yService)
778                     a11yEnabledServices = a11yEnabledServices.filter {
779                         it.id != a11yService.flattenToShortString()
780                     }
781                     SafetyEvent.Builder(
782                         SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED
783                     )
784                 } catch (ex: Exception) {
785                     Log.w(LOG_TAG, "error occurred in disabling a11y service.", ex)
786                     SafetyEvent.Builder(
787                         SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED
788                     )
789                 }
790                 val safetySourceIssueId = intent.getStringExtra(
791                     SafetyCenterManager.EXTRA_SAFETY_SOURCE_ISSUE_ID
792                 )
793                 val safetyEvent = builder.setSafetySourceIssueId(safetySourceIssueId)
794                     .setSafetySourceIssueActionId(SC_ACCESSIBILITY_REMOVE_ACCESS_ACTION_ID)
795                     .build()
796                 accessibilityService.sendIssuesToSafetyCenter(a11yEnabledServices, safetyEvent)
797             }
798             if (DEBUG) {
799                 Log.v(LOG_TAG, "ISSUE_CARD_INTERACTION CTA1 metric, uid $uid session $sessionId")
800             }
801             PermissionControllerStatsLog.write(
802                 PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION,
803                 PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__A11Y_SERVICE,
804                 uid,
805                 PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CLICKED_CTA1,
806                 sessionId
807             )
808         }
809     }
810 }
811 
812 /**
813  * Handler for accessibility warning cards dismissal in safety center dashboard
814  */
815 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
816 class AccessibilityWarningCardDismissalReceiver : BroadcastReceiver() {
817     private val LOG_TAG = AccessibilityWarningCardDismissalReceiver::class.java.simpleName
818 
onReceivenull819     override fun onReceive(context: Context, intent: Intent) {
820         val componentName =
821             Utils.getParcelableExtraSafe<ComponentName>(intent, Intent.EXTRA_COMPONENT_NAME)
822         val sessionId =
823             intent.getLongExtra(Constants.EXTRA_SESSION_ID, Constants.INVALID_SESSION_ID)
824         val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
825         val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
826         coroutineScope.launch(Dispatchers.Default) {
827             if (DEBUG) {
828                 Log.v(LOG_TAG, "removing notification for ${componentName.flattenToShortString()}")
829             }
830             val accessibilityService = AccessibilitySourceService(context)
831             accessibilityService.removeAccessibilityNotification(componentName)
832             accessibilityService.markServiceAsNotified(componentName)
833         }
834 
835         if (DEBUG) {
836             Log.v(LOG_TAG, "ISSUE_CARD_INTERACTION DISMISSED metric, uid $uid session $sessionId")
837         }
838         PermissionControllerStatsLog.write(
839             PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION,
840             PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__A11Y_SERVICE,
841             uid,
842             PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CARD_DISMISSED,
843             sessionId
844         )
845     }
846 }
847 
848 /**
849  * Schedules periodic job to send notifications for third part accessibility services,
850  * the job also sends this data to Safety Center.
851  */
852 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
853 class AccessibilityOnBootReceiver : BroadcastReceiver() {
854     private val LOG_TAG = AccessibilityOnBootReceiver::class.java.simpleName
855 
onReceivenull856     override fun onReceive(context: Context, intent: Intent) {
857         if (!isAccessibilitySourceSupported() || isProfile(context)) {
858             Log.v(LOG_TAG, "accessibility privacy job not supported, can't schedule the job")
859             return
860         }
861         if (DEBUG) {
862             Log.v(LOG_TAG, "scheduling safety center accessibility privacy source job")
863         }
864 
865         val jobScheduler = getSystemServiceSafe(context, JobScheduler::class.java)
866 
867         if (jobScheduler.getPendingJob(Constants.PERIODIC_ACCESSIBILITY_CHECK_JOB_ID) == null) {
868             val jobInfo = JobInfo.Builder(
869                 Constants.PERIODIC_ACCESSIBILITY_CHECK_JOB_ID,
870                 ComponentName(context, AccessibilityJobService::class.java)
871             )
872                 .setPeriodic(
873                     AccessibilitySourceService.getJobsIntervalMillis(),
874                     AccessibilitySourceService.getFlexJobsIntervalMillis()
875                 )
876                 .build()
877 
878             val status = jobScheduler.schedule(jobInfo)
879             if (status != JobScheduler.RESULT_SUCCESS) {
880                 Log.w(LOG_TAG, "Could not schedule AccessibilityJobService: $status")
881             }
882         }
883     }
884 }
885 
886 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
887 class AccessibilityJobService : JobService() {
888     private val LOG_TAG = AccessibilityJobService::class.java.simpleName
889 
890     private var mSourceService: AccessibilitySourceService? = null
891     private val mLock = Object()
892 
893     @GuardedBy("mLock")
894     private var mCurrentJob: Job? = null
895 
onCreatenull896     override fun onCreate() {
897         super.onCreate()
898         Log.v(LOG_TAG, "accessibility privacy source job created.")
899         mSourceService = AccessibilitySourceService(this)
900     }
901 
onStartJobnull902     override fun onStartJob(params: JobParameters?): Boolean {
903         Log.v(LOG_TAG, "accessibility privacy source job started.")
904         synchronized(mLock) {
905             if (mCurrentJob != null) {
906                 Log.v(LOG_TAG, "Accessibility privacy source job already running")
907                 return false
908             }
909             if (!isAccessibilitySourceEnabled() ||
910                 !isSafetyCenterEnabled(this@AccessibilityJobService)) {
911                 Log.v(LOG_TAG, "either privacy source or safety center is not enabled")
912                 jobFinished(params, false)
913                 mCurrentJob = null
914                 return false
915             }
916             val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
917             mCurrentJob = coroutineScope.launch(Dispatchers.Default) {
918                 mSourceService?.processAccessibilityJob(
919                     params,
920                     this@AccessibilityJobService,
921                     BooleanSupplier {
922                         val job = mCurrentJob
923                         return@BooleanSupplier job?.isCancelled ?: false
924                     }
925                 ) ?: jobFinished(params, false)
926             }
927         }
928         return true
929     }
930 
onStopJobnull931     override fun onStopJob(params: JobParameters?): Boolean {
932         var job: Job?
933         synchronized(mLock) {
934             job = if (mCurrentJob == null) {
935                 return false
936             } else {
937                 mCurrentJob
938             }
939         }
940         job?.cancel()
941         return false
942     }
943 
clearJobnull944     fun clearJob() {
945         synchronized(mLock) {
946             mCurrentJob = null
947         }
948     }
949 }
950 
951 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
952 class SafetyCenterAccessibilityListener(val context: Context) :
953     AccessibilityManager.AccessibilityServicesStateChangeListener {
954 
955     private val LOG_TAG = SafetyCenterAccessibilityListener::class.java.simpleName
956 
onAccessibilityServicesStateChangednull957     override fun onAccessibilityServicesStateChanged(manager: AccessibilityManager) {
958         if (!isAccessibilityListenerEnabled()) {
959             Log.v(LOG_TAG, "accessibility event occurred, listener not enabled.")
960             return
961         }
962 
963         if (!isAccessibilitySourceEnabled() || !isSafetyCenterEnabled(context) ||
964             isProfile(context)) {
965             Log.v(LOG_TAG, "accessibility event occurred, safety center feature not enabled.")
966             return
967         }
968 
969         val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
970         coroutineScope.launch(Dispatchers.Default) {
971             if (DEBUG) {
972                 Log.v(LOG_TAG, "processing accessibility event")
973             }
974             AccessibilitySourceService.lock.withLock {
975                 val a11ySourceService = AccessibilitySourceService(context)
976                 val a11yEnabledServices = a11ySourceService.getEnabledAccessibilityServices()
977                 a11ySourceService.sendIssuesToSafetyCenter(a11yEnabledServices)
978                 val enabledComponents = a11yEnabledServices.map { a11yService ->
979                     ComponentName.unflattenFromString(a11yService.id)!!.flattenToShortString()
980                 }.toSet()
981                 a11ySourceService.removeAccessibilityNotification(enabledComponents)
982                 a11ySourceService.updateServiceAsNotified(enabledComponents)
983             }
984         }
985     }
986 }
987