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