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