7
7
package com.datadog.android.rum.internal.domain.accessibility
8
8
9
9
import android.app.ActivityManager
10
+ import android.content.ComponentCallbacks
10
11
import android.content.Context
12
+ import android.content.res.Configuration
11
13
import android.content.res.Resources
14
+ import android.database.ContentObserver
15
+ import android.net.Uri
12
16
import android.os.Build
17
+ import android.os.Handler
18
+ import android.os.Looper
13
19
import android.provider.Settings
14
20
import android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED
15
21
import android.view.accessibility.AccessibilityManager
16
- import androidx.annotation.VisibleForTesting
22
+ import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
17
23
import com.datadog.android.api.InternalLogger
18
- import java.util.concurrent.atomic.AtomicInteger
24
+ import java.util.concurrent.atomic.AtomicBoolean
25
+ import java.util.concurrent.atomic.AtomicLong
19
26
27
+ @Suppress(" TooManyFunctions" )
20
28
internal class DatadogAccessibilityReader (
21
29
private val internalLogger : InternalLogger ,
22
30
private val applicationContext : Context ,
@@ -27,49 +35,133 @@ internal class DatadogAccessibilityReader(
27
35
applicationContext.getSystemService(Context .ACCESSIBILITY_SERVICE ) as ? AccessibilityManager ,
28
36
private val secureWrapper : SecureWrapper = SecureWrapper (),
29
37
private val globalWrapper : GlobalWrapper = GlobalWrapper (),
30
- private val cacheTimeoutMilliseconds : Int = CACHE_TIMEOUT_MILLISECONDS
31
- ) : AccessibilityReader {
32
-
33
- private var cacheMisses = AtomicInteger (0 )
38
+ private val handler : Handler = Handler (Looper .getMainLooper())
39
+ ) : AccessibilityReader, ComponentCallbacks {
34
40
35
41
@Volatile
36
- private var cachedAccessibilityState: Pair <Long , Accessibility >? = null
42
+ private var currentState = Accessibility .EMPTY_STATE
43
+
44
+ private var lastPollTime: AtomicLong = AtomicLong (0 )
45
+
46
+ private var isInitialized = AtomicBoolean (false )
47
+
48
+ private val displayInversionListener = object : ContentObserver (handler) {
49
+ override fun onChange (selfChange : Boolean , uri : Uri ? ) {
50
+ val newDisplayInversion = isDisplayInversionEnabled()
51
+ updateState { it.copy(isColorInversionEnabled = newDisplayInversion) }
52
+ }
53
+ }
54
+
55
+ private val captioningListener = object : ContentObserver (handler) {
56
+ override fun onChange (selfChange : Boolean , uri : Uri ? ) {
57
+ val newCaptioningState = isClosedCaptioningEnabled()
58
+ updateState { it.copy(isClosedCaptioningEnabled = newCaptioningState) }
59
+ }
60
+ }
61
+
62
+ private val animationDurationListener = object : ContentObserver (handler) {
63
+ override fun onChange (selfChange : Boolean , uri : Uri ? ) {
64
+ val newReducedAnimationsEnabled = isReducedAnimationsEnabled()
65
+ updateState { it.copy(isReducedAnimationsEnabled = newReducedAnimationsEnabled) }
66
+ }
67
+ }
68
+
69
+ private val touchListener = TouchExplorationStateChangeListener {
70
+ val newScreenReaderEnabled = isScreenReaderEnabled(accessibilityManager)
71
+ updateState { it.copy(isScreenReaderEnabled = newScreenReaderEnabled) }
72
+ }
73
+
74
+ override fun cleanup () {
75
+ if (isInitialized.get()) {
76
+ accessibilityManager?.removeTouchExplorationStateChangeListener(touchListener)
77
+ applicationContext.contentResolver.unregisterContentObserver(animationDurationListener)
78
+ applicationContext.contentResolver.unregisterContentObserver(captioningListener)
79
+ applicationContext.contentResolver.unregisterContentObserver(displayInversionListener)
80
+ applicationContext.unregisterComponentCallbacks(this )
81
+ isInitialized.set(false )
82
+ }
83
+ }
84
+
85
+ override fun onLowMemory () {
86
+ // do nothing - there's nothing we're holding onto that takes up any significant memory
87
+ }
88
+
89
+ override fun onConfigurationChanged (p0 : Configuration ) {
90
+ val newTextSize = getTextSize()
91
+ updateState { it.copy(textSize = newTextSize) }
92
+ }
37
93
38
94
@Synchronized
39
95
override fun getState (): Map <String , Any > {
40
- val (cacheTime, cacheValue) = cachedAccessibilityState ? : Pair ( null , null )
96
+ ensureInitialized( )
41
97
42
- return if (cacheValue != null && cacheTime != null && shouldUseCache(cacheTime)) {
43
- cacheValue.toMap()
44
- } else {
45
- cacheMisses.incrementAndGet()
46
- val accessibilityManager = accessibilityManager ? : return Accessibility .EMPTY_STATE .toMap()
98
+ val currentTime = System .currentTimeMillis()
99
+ val shouldPoll = currentTime - lastPollTime.get() >= POLL_THRESHOLD
100
+ if (shouldPoll) {
101
+ lastPollTime.set(currentTime)
102
+ pollForAttributesWithoutListeners()
103
+ }
47
104
48
- val accessibilityState = Accessibility (
49
- textSize = getTextSize(),
50
- isScreenReaderEnabled = getScreenReaderEnabled(accessibilityManager),
51
- isColorInversionEnabled = getSecureInt(ACCESSIBILITY_DISPLAY_INVERSION_ENABLED ),
52
- isClosedCaptioningEnabled = getSecureInt(CAPTIONING_ENABLED_KEY ),
53
- isReducedAnimationsEnabled = getReducedAnimationsEnabled(),
54
- isScreenPinningEnabled = getLockToScreenEnabled()
55
- )
105
+ return currentState.toMap()
106
+ }
56
107
57
- cachedAccessibilityState = Pair (System .currentTimeMillis(), accessibilityState)
108
+ @Synchronized
109
+ private fun updateState (updater : (Accessibility ) -> Accessibility ) {
110
+ currentState = updater(currentState)
111
+ }
58
112
59
- accessibilityState.toMap()
113
+ private fun ensureInitialized () {
114
+ if (! isInitialized.get()) {
115
+ registerListeners()
116
+ currentState = buildInitialState()
117
+ isInitialized.set(true )
60
118
}
61
119
}
62
120
63
- @VisibleForTesting
64
- internal fun numCacheMisses (): Int {
65
- return cacheMisses.get()
121
+ private fun buildInitialState (): Accessibility {
122
+ return Accessibility (
123
+ textSize = getTextSize(),
124
+ isScreenReaderEnabled = isScreenReaderEnabled(accessibilityManager),
125
+ isColorInversionEnabled = isDisplayInversionEnabled(),
126
+ isScreenPinningEnabled = isLockToScreenEnabled(),
127
+ isReducedAnimationsEnabled = isReducedAnimationsEnabled(),
128
+ isClosedCaptioningEnabled = isClosedCaptioningEnabled()
129
+ )
130
+ }
131
+
132
+ private fun registerListeners () {
133
+ applicationContext.registerComponentCallbacks(this )
134
+
135
+ applicationContext.contentResolver.registerContentObserver(
136
+ Settings .Secure .getUriFor(ACCESSIBILITY_DISPLAY_INVERSION_ENABLED ),
137
+ false ,
138
+ displayInversionListener
139
+ )
140
+
141
+ applicationContext.contentResolver.registerContentObserver(
142
+ Settings .Secure .getUriFor(CAPTIONING_ENABLED_KEY ),
143
+ false ,
144
+ captioningListener
145
+ )
146
+
147
+ applicationContext.contentResolver.registerContentObserver(
148
+ Settings .Global .getUriFor(Settings .Global .ANIMATOR_DURATION_SCALE ),
149
+ false ,
150
+ animationDurationListener
151
+ )
152
+
153
+ accessibilityManager?.addTouchExplorationStateChangeListener(touchListener)
154
+ }
155
+
156
+ private fun isDisplayInversionEnabled (): Boolean? {
157
+ return getSecureInt(ACCESSIBILITY_DISPLAY_INVERSION_ENABLED )
66
158
}
67
159
68
- private fun shouldUseCache ( cacheTime : Long ): Boolean {
69
- return System .currentTimeMillis() - cacheTime < cacheTimeoutMilliseconds
160
+ private fun isClosedCaptioningEnabled ( ): Boolean? {
161
+ return getSecureInt( CAPTIONING_ENABLED_KEY )
70
162
}
71
163
72
- private fun getLockToScreenEnabled (): Boolean? {
164
+ private fun isLockToScreenEnabled (): Boolean? {
73
165
val localManager = activityManager ? : return null
74
166
75
167
return if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
@@ -80,7 +172,7 @@ internal class DatadogAccessibilityReader(
80
172
}
81
173
}
82
174
83
- private fun getReducedAnimationsEnabled (): Boolean? {
175
+ private fun isReducedAnimationsEnabled (): Boolean? {
84
176
return globalWrapper.getFloat(
85
177
applicationContext = applicationContext,
86
178
internalLogger = internalLogger,
@@ -102,15 +194,20 @@ internal class DatadogAccessibilityReader(
102
194
return resources?.configuration?.fontScale
103
195
}
104
196
105
- private fun getScreenReaderEnabled (accessibilityManager : AccessibilityManager ): Boolean {
106
- return accessibilityManager.isTouchExplorationEnabled
197
+ private fun isScreenReaderEnabled (accessibilityManager : AccessibilityManager ? ): Boolean? {
198
+ return accessibilityManager?.isTouchExplorationEnabled
199
+ }
200
+
201
+ private fun pollForAttributesWithoutListeners () {
202
+ val newLockScreenEnabled = isLockToScreenEnabled()
203
+ updateState { it.copy(isScreenPinningEnabled = newLockScreenEnabled) }
107
204
}
108
205
109
206
internal companion object {
110
207
// https://android.googlesource.com/platform/frameworks/base/+/android-4.4.2_r2/core/java/android/provider/Settings.java
111
208
internal const val CAPTIONING_ENABLED_KEY = " accessibility_captioning_enabled"
112
209
113
- // Check the accessibility state not more than once every 30 seconds
114
- private const val CACHE_TIMEOUT_MILLISECONDS = 30_000
210
+ // don't poll more than once in 30 seconds for attributes without listeners
211
+ internal const val POLL_THRESHOLD = 30_000L
115
212
}
116
213
}
0 commit comments