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