Skip to content

Commit 2232142

Browse files
committed
RUM-9631: Use a hybrid listener-polling approach for attributes
1 parent c065b0a commit 2232142

File tree

7 files changed

+595
-81
lines changed

7 files changed

+595
-81
lines changed

detekt_custom.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ datadog:
369369
- "android.content.Context.registerReceiver(android.content.BroadcastReceiver?, android.content.IntentFilter?)"
370370
- "android.content.Context.registerReceiver(android.content.BroadcastReceiver?, android.content.IntentFilter?, kotlin.Int)"
371371
- "android.content.Context.resourceIdName(kotlin.Int)"
372+
- "android.content.Context.unregisterComponentCallbacks(android.content.ComponentCallbacks?)"
372373
- "android.content.Context.unregisterReceiver(android.content.BroadcastReceiver?)"
373374
- "android.content.Intent.getBooleanExtra(kotlin.String?, kotlin.Boolean)"
374375
- "android.content.Intent.getIntExtra(kotlin.String?, kotlin.Int)"
@@ -406,12 +407,17 @@ datadog:
406407
- "android.os.StrictMode.allowThreadDiskWrites()"
407408
- "android.os.StrictMode.setThreadPolicy(android.os.StrictMode.ThreadPolicy?)"
408409
- "android.os.SystemClock.elapsedRealtime()"
410+
- "android.provider.Settings.Global.getUriFor(kotlin.String?)"
411+
- "android.provider.Settings.Secure.getUriFor(kotlin.String?)"
409412
- "android.util.Log.e(kotlin.String?, kotlin.String)"
410413
- "android.util.Log.e(kotlin.String?, kotlin.String?, kotlin.Throwable?)"
411414
- "android.util.Log.getStackTraceString(kotlin.Throwable?)"
412415
- "android.util.Log.i(kotlin.String?, kotlin.String)"
413416
- "android.util.Log.w(kotlin.String?, kotlin.String)"
414417
- "android.util.TypedValue.constructor()"
418+
- "android.view.accessibility.AccessibilityManager.addTouchExplorationStateChangeListener(android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener)"
419+
- "android.view.accessibility.AccessibilityManager.removeTouchExplorationStateChangeListener(android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener)"
420+
- "android.view.accessibility.TouchExplorationStateChangeListener(kotlin.Function1)"
415421
- "android.view.Choreographer.postFrameCallback(android.view.Choreographer.FrameCallback)"
416422
- "android.view.Display.getSize(android.graphics.Point?)"
417423
- "android.view.FrameMetrics.getMetric(kotlin.Int)"
@@ -554,6 +560,9 @@ datadog:
554560
- "androidx.compose.ui.semantics.SemanticsConfiguration.getOrNull(androidx.compose.ui.semantics.SemanticsPropertyKey)"
555561
- "androidx.compose.ui.semantics.SemanticsPropertyKey.constructor(kotlin.String, kotlin.Function2)"
556562
- "androidx.compose.ui.text.AnnotatedString.getStringAnnotations(kotlin.Int, kotlin.Int)"
563+
- "android.content.ContentResolver.registerContentObserver(android.net.Uri, kotlin.Boolean, android.database.ContentObserver)"
564+
- "android.content.ContentResolver.unregisterContentObserver(android.database.ContentObserver)"
565+
- "android.content.res.Resources.getSystem()"
557566
- "androidx.core.view.GestureDetectorCompat.constructor(android.content.Context, android.view.GestureDetector.OnGestureListener)"
558567
- "androidx.core.view.GestureDetectorCompat.onTouchEvent(android.view.MotionEvent)"
559568
- "androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks.onFragmentActivityCreated(androidx.fragment.app.FragmentManager, androidx.fragment.app.Fragment, android.os.Bundle?)"

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ internal class RumFeature(
299299
anrDetectorRunnable?.stop()
300300
vitalExecutorService = NoOpScheduledExecutorService()
301301
sessionListener = NoOpRumSessionListener()
302+
accessibilityReader.cleanup()
302303

303304
GlobalRumMonitor.unregister(sdkCore)
304305
}

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/AccessibilityReader.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import com.datadog.tools.annotation.NoOpImplementation
1212
internal interface AccessibilityReader {
1313
fun getState(): Map<String, Any>
1414

15+
fun cleanup()
16+
1517
companion object {
1618
internal const val ACCESSIBILITY_KEY = "accessibility"
1719
}

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/DatadogAccessibilityReader.kt

Lines changed: 131 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,24 @@
77
package com.datadog.android.rum.internal.domain.accessibility
88

99
import android.app.ActivityManager
10+
import android.content.ComponentCallbacks
1011
import android.content.Context
12+
import android.content.res.Configuration
1113
import android.content.res.Resources
14+
import android.database.ContentObserver
15+
import android.net.Uri
1216
import android.os.Build
17+
import android.os.Handler
18+
import android.os.Looper
1319
import android.provider.Settings
1420
import android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED
1521
import android.view.accessibility.AccessibilityManager
16-
import androidx.annotation.VisibleForTesting
22+
import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
1723
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
1926

27+
@Suppress("TooManyFunctions")
2028
internal class DatadogAccessibilityReader(
2129
private val internalLogger: InternalLogger,
2230
private val applicationContext: Context,
@@ -27,49 +35,133 @@ internal class DatadogAccessibilityReader(
2735
applicationContext.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager,
2836
private val secureWrapper: SecureWrapper = SecureWrapper(),
2937
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 {
3440

3541
@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+
}
3793

3894
@Synchronized
3995
override fun getState(): Map<String, Any> {
40-
val (cacheTime, cacheValue) = cachedAccessibilityState ?: Pair(null, null)
96+
ensureInitialized()
4197

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+
}
47104

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+
}
56107

57-
cachedAccessibilityState = Pair(System.currentTimeMillis(), accessibilityState)
108+
@Synchronized
109+
private fun updateState(updater: (Accessibility) -> Accessibility) {
110+
currentState = updater(currentState)
111+
}
58112

59-
accessibilityState.toMap()
113+
private fun ensureInitialized() {
114+
if (!isInitialized.get()) {
115+
registerListeners()
116+
currentState = buildInitialState()
117+
isInitialized.set(true)
60118
}
61119
}
62120

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)
66158
}
67159

68-
private fun shouldUseCache(cacheTime: Long): Boolean {
69-
return System.currentTimeMillis() - cacheTime < cacheTimeoutMilliseconds
160+
private fun isClosedCaptioningEnabled(): Boolean? {
161+
return getSecureInt(CAPTIONING_ENABLED_KEY)
70162
}
71163

72-
private fun getLockToScreenEnabled(): Boolean? {
164+
private fun isLockToScreenEnabled(): Boolean? {
73165
val localManager = activityManager ?: return null
74166

75167
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -80,7 +172,7 @@ internal class DatadogAccessibilityReader(
80172
}
81173
}
82174

83-
private fun getReducedAnimationsEnabled(): Boolean? {
175+
private fun isReducedAnimationsEnabled(): Boolean? {
84176
return globalWrapper.getFloat(
85177
applicationContext = applicationContext,
86178
internalLogger = internalLogger,
@@ -102,15 +194,20 @@ internal class DatadogAccessibilityReader(
102194
return resources?.configuration?.fontScale
103195
}
104196

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) }
107204
}
108205

109206
internal companion object {
110207
// https://android.googlesource.com/platform/frameworks/base/+/android-4.4.2_r2/core/java/android/provider/Settings.java
111208
internal const val CAPTIONING_ENABLED_KEY = "accessibility_captioning_enabled"
112209

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
115212
}
116213
}

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/GlobalWrapper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import android.provider.Settings.SettingNotFoundException
1212
import com.datadog.android.api.InternalLogger
1313

1414
internal class GlobalWrapper {
15-
@Suppress("TooGenericExceptionCaught")
15+
@Suppress("TooGenericExceptionCaught", "UnsafeThirdPartyFunctionCall") // exceptions caught
1616
internal fun getFloat(
1717
internalLogger: InternalLogger,
1818
applicationContext: Context,

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/SecureWrapper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import android.provider.Settings.SettingNotFoundException
1212
import com.datadog.android.api.InternalLogger
1313

1414
internal class SecureWrapper {
15-
@Suppress("TooGenericExceptionCaught")
15+
@Suppress("TooGenericExceptionCaught", "UnsafeThirdPartyFunctionCall") // exceptions caught
1616
internal fun getInt(
1717
internalLogger: InternalLogger,
1818
applicationContext: Context,

0 commit comments

Comments
 (0)