Skip to content

Commit c065b0a

Browse files
committed
RUM-9631: Add accessibility attributes
1 parent 3ebb2b5 commit c065b0a

File tree

25 files changed

+1130
-53
lines changed

25 files changed

+1130
-53
lines changed

features/dd-sdk-android-rum/api/apiSurface

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ data class com.datadog.android.rum.RumConfiguration
8888
fun setSessionListener(RumSessionListener): Builder
8989
fun setInitialResourceIdentifier(com.datadog.android.rum.metric.networksettled.InitialResourceIdentifier): Builder
9090
fun setLastInteractionIdentifier(com.datadog.android.rum.metric.interactiontonextview.LastInteractionIdentifier?): Builder
91+
fun collectAccessibilitySettings(): Builder
9192
fun setSlowFramesConfiguration(com.datadog.android.rum.configuration.SlowFramesConfiguration?): Builder
9293
fun trackAnonymousUser(Boolean): Builder
9394
fun build(): RumConfiguration

features/dd-sdk-android-rum/api/dd-sdk-android-rum.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public final class com/datadog/android/rum/RumConfiguration {
104104
public final class com/datadog/android/rum/RumConfiguration$Builder {
105105
public fun <init> (Ljava/lang/String;)V
106106
public final fun build ()Lcom/datadog/android/rum/RumConfiguration;
107+
public final fun collectAccessibilitySettings ()Lcom/datadog/android/rum/RumConfiguration$Builder;
107108
public final fun disableUserInteractionTracking ()Lcom/datadog/android/rum/RumConfiguration$Builder;
108109
public final fun setActionEventMapper (Lcom/datadog/android/event/EventMapper;)Lcom/datadog/android/rum/RumConfiguration$Builder;
109110
public final fun setErrorEventMapper (Lcom/datadog/android/event/EventMapper;)Lcom/datadog/android/rum/RumConfiguration$Builder;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ object Rum {
140140
initialResourceIdentifier = rumFeature.initialResourceIdentifier,
141141
lastInteractionIdentifier = rumFeature.lastInteractionIdentifier,
142142
slowFramesListener = rumFeature.slowFramesListener,
143-
rumSessionTypeOverride = rumFeature.configuration.rumSessionTypeOverride
143+
rumSessionTypeOverride = rumFeature.configuration.rumSessionTypeOverride,
144+
accessibilityReader = rumFeature.accessibilityReader
144145
)
145146
}
146147

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,14 @@ data class RumConfiguration internal constructor(
285285
return this
286286
}
287287

288+
/**
289+
* Sets a flag to collect accessibility settings inside the RUM view end event.
290+
*/
291+
fun collectAccessibilitySettings(): Builder {
292+
rumConfig = rumConfig.copy(collectAccessibilitySettings = true)
293+
return this
294+
}
295+
288296
/**
289297
* The [SlowFramesListener] provides statistical data to help identify performance issues related to UI rendering:
290298
*

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ import com.datadog.android.rum.configuration.VitalsUpdateFrequency
4343
import com.datadog.android.rum.internal.anr.ANRDetectorRunnable
4444
import com.datadog.android.rum.internal.debug.UiRumDebugListener
4545
import com.datadog.android.rum.internal.domain.RumDataWriter
46+
import com.datadog.android.rum.internal.domain.accessibility.AccessibilityReader
47+
import com.datadog.android.rum.internal.domain.accessibility.DatadogAccessibilityReader
48+
import com.datadog.android.rum.internal.domain.accessibility.NoOpAccessibilityReader
4649
import com.datadog.android.rum.internal.domain.event.RumEventMapper
4750
import com.datadog.android.rum.internal.domain.event.RumEventMetaDeserializer
4851
import com.datadog.android.rum.internal.domain.event.RumEventMetaSerializer
@@ -145,15 +148,23 @@ internal class RumFeature(
145148
internal var initialResourceIdentifier: InitialResourceIdentifier = NoOpInitialResourceIdentifier()
146149
internal var lastInteractionIdentifier: LastInteractionIdentifier? = NoOpLastInteractionIdentifier()
147150
internal var slowFramesListener: SlowFramesListener? = null
151+
internal var accessibilityReader: AccessibilityReader = NoOpAccessibilityReader()
148152

149153
private val lateCrashEventHandler by lazy { lateCrashReporterFactory(sdkCore as InternalSdkCore) }
150154

151155
// region Feature
152156

153157
override val name: String = Feature.RUM_FEATURE_NAME
154158

159+
@Suppress("LongMethod")
155160
override fun onInitialize(appContext: Context) {
156161
this.appContext = appContext
162+
163+
if (configuration.collectAccessibilitySettings) {
164+
accessibilityReader =
165+
DatadogAccessibilityReader(applicationContext = appContext, internalLogger = sdkCore.internalLogger)
166+
}
167+
157168
initialResourceIdentifier = configuration.initialResourceIdentifier
158169
lastInteractionIdentifier = configuration.lastInteractionIdentifier
159170

@@ -630,7 +641,8 @@ internal class RumFeature(
630641
val composeActionTrackingStrategy: ActionTrackingStrategy,
631642
val additionalConfig: Map<String, Any>,
632643
val trackAnonymousUser: Boolean,
633-
val rumSessionTypeOverride: RumSessionType?
644+
val rumSessionTypeOverride: RumSessionType?,
645+
val collectAccessibilitySettings: Boolean
634646
)
635647

636648
internal companion object {
@@ -680,7 +692,8 @@ internal class RumFeature(
680692
additionalConfig = emptyMap(),
681693
trackAnonymousUser = true,
682694
slowFramesConfiguration = null,
683-
rumSessionTypeOverride = null
695+
rumSessionTypeOverride = null,
696+
collectAccessibilitySettings = false
684697
)
685698

686699
internal const val EVENT_MESSAGE_PROPERTY = "message"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.rum.internal.domain.accessibility
8+
9+
/**
10+
* Represents the accessibility settings state of the device.
11+
*
12+
* @property textSize The font scale factor (1.0 = normal, >1.0 = larger, <1.0 = smaller)
13+
* @property isScreenReaderEnabled Whether touch exploration is enabled (TalkBack, etc.)
14+
* @property isColorInversionEnabled Whether color inversion is enabled
15+
* @property isClosedCaptioningEnabled Whether closed captions are enabled
16+
* @property isReducedAnimationsEnabled Whether animations are disabled/reduced
17+
* @property isScreenPinningEnabled Whether the device is in single-app mode
18+
*/
19+
internal data class Accessibility(
20+
val textSize: Float? = null,
21+
val isScreenReaderEnabled: Boolean? = null,
22+
val isColorInversionEnabled: Boolean? = null,
23+
val isClosedCaptioningEnabled: Boolean? = null,
24+
val isReducedAnimationsEnabled: Boolean? = null,
25+
val isScreenPinningEnabled: Boolean? = null
26+
) {
27+
fun toMap(): Map<String, Any> = buildMap {
28+
textSize?.let { put(TEXT_SIZE_KEY, it) }
29+
isScreenReaderEnabled?.let { put(SCREEN_READER_ENABLED_KEY, it) }
30+
isColorInversionEnabled?.let { put(COLOR_INVERSION_ENABLED_KEY, it) }
31+
isClosedCaptioningEnabled?.let { put(CLOSED_CAPTIONING_ENABLED_KEY, it) }
32+
isReducedAnimationsEnabled?.let { put(REDUCED_ANIMATIONS_ENABLED_KEY, it) }
33+
isScreenPinningEnabled?.let { put(SCREEN_PINNING_ENABLED_KEY, it) }
34+
}
35+
36+
companion object {
37+
internal val EMPTY_STATE = Accessibility(
38+
textSize = null,
39+
isScreenReaderEnabled = null,
40+
isColorInversionEnabled = null,
41+
isClosedCaptioningEnabled = null,
42+
isReducedAnimationsEnabled = null,
43+
isScreenPinningEnabled = null
44+
)
45+
46+
internal const val TEXT_SIZE_KEY = "text_size"
47+
internal const val SCREEN_READER_ENABLED_KEY = "screen_reader_enabled"
48+
internal const val COLOR_INVERSION_ENABLED_KEY = "invert_colors_enabled"
49+
internal const val CLOSED_CAPTIONING_ENABLED_KEY = "closed_captioning_enabled"
50+
internal const val REDUCED_ANIMATIONS_ENABLED_KEY = "reduced_animations_enabled"
51+
internal const val SCREEN_PINNING_ENABLED_KEY = "single_app_mode_enabled"
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.rum.internal.domain.accessibility
8+
9+
import com.datadog.tools.annotation.NoOpImplementation
10+
11+
@NoOpImplementation
12+
internal interface AccessibilityReader {
13+
fun getState(): Map<String, Any>
14+
15+
companion object {
16+
internal const val ACCESSIBILITY_KEY = "accessibility"
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.rum.internal.domain.accessibility
8+
9+
import android.app.ActivityManager
10+
import android.content.Context
11+
import android.content.res.Resources
12+
import android.os.Build
13+
import android.provider.Settings
14+
import android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED
15+
import android.view.accessibility.AccessibilityManager
16+
import androidx.annotation.VisibleForTesting
17+
import com.datadog.android.api.InternalLogger
18+
import java.util.concurrent.atomic.AtomicInteger
19+
20+
internal class DatadogAccessibilityReader(
21+
private val internalLogger: InternalLogger,
22+
private val applicationContext: Context,
23+
private val resources: Resources? = Resources.getSystem(),
24+
private val activityManager: ActivityManager? =
25+
applicationContext.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager,
26+
private val accessibilityManager: AccessibilityManager? =
27+
applicationContext.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager,
28+
private val secureWrapper: SecureWrapper = SecureWrapper(),
29+
private val globalWrapper: GlobalWrapper = GlobalWrapper(),
30+
private val cacheTimeoutMilliseconds: Int = CACHE_TIMEOUT_MILLISECONDS
31+
) : AccessibilityReader {
32+
33+
private var cacheMisses = AtomicInteger(0)
34+
35+
@Volatile
36+
private var cachedAccessibilityState: Pair<Long, Accessibility>? = null
37+
38+
@Synchronized
39+
override fun getState(): Map<String, Any> {
40+
val (cacheTime, cacheValue) = cachedAccessibilityState ?: Pair(null, null)
41+
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()
47+
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+
)
56+
57+
cachedAccessibilityState = Pair(System.currentTimeMillis(), accessibilityState)
58+
59+
accessibilityState.toMap()
60+
}
61+
}
62+
63+
@VisibleForTesting
64+
internal fun numCacheMisses(): Int {
65+
return cacheMisses.get()
66+
}
67+
68+
private fun shouldUseCache(cacheTime: Long): Boolean {
69+
return System.currentTimeMillis() - cacheTime < cacheTimeoutMilliseconds
70+
}
71+
72+
private fun getLockToScreenEnabled(): Boolean? {
73+
val localManager = activityManager ?: return null
74+
75+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
76+
localManager.lockTaskModeState != ActivityManager.LOCK_TASK_MODE_NONE
77+
} else {
78+
@Suppress("DEPRECATION")
79+
localManager.isInLockTaskMode
80+
}
81+
}
82+
83+
private fun getReducedAnimationsEnabled(): Boolean? {
84+
return globalWrapper.getFloat(
85+
applicationContext = applicationContext,
86+
internalLogger = internalLogger,
87+
key = Settings.Global.ANIMATOR_DURATION_SCALE
88+
)?.let {
89+
it == 0.0f
90+
}
91+
}
92+
93+
private fun getSecureInt(key: String): Boolean? {
94+
return secureWrapper.getInt(
95+
internalLogger = internalLogger,
96+
applicationContext = applicationContext,
97+
key = key
98+
)
99+
}
100+
101+
private fun getTextSize(): Float? {
102+
return resources?.configuration?.fontScale
103+
}
104+
105+
private fun getScreenReaderEnabled(accessibilityManager: AccessibilityManager): Boolean {
106+
return accessibilityManager.isTouchExplorationEnabled
107+
}
108+
109+
internal companion object {
110+
// https://android.googlesource.com/platform/frameworks/base/+/android-4.4.2_r2/core/java/android/provider/Settings.java
111+
internal const val CAPTIONING_ENABLED_KEY = "accessibility_captioning_enabled"
112+
113+
// Check the accessibility state not more than once every 30 seconds
114+
private const val CACHE_TIMEOUT_MILLISECONDS = 30_000
115+
}
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.rum.internal.domain.accessibility
8+
9+
import android.content.Context
10+
import android.provider.Settings
11+
import android.provider.Settings.SettingNotFoundException
12+
import com.datadog.android.api.InternalLogger
13+
14+
internal class GlobalWrapper {
15+
@Suppress("TooGenericExceptionCaught")
16+
internal fun getFloat(
17+
internalLogger: InternalLogger,
18+
applicationContext: Context,
19+
key: String
20+
): Float? {
21+
return try {
22+
Settings.Global.getFloat(
23+
applicationContext.contentResolver,
24+
key
25+
)
26+
} catch (e: SettingNotFoundException) {
27+
internalLogger.log(
28+
InternalLogger.Level.ERROR,
29+
listOf(InternalLogger.Target.MAINTAINER),
30+
{ "Setting not found $key" },
31+
e
32+
)
33+
null
34+
} catch (e: NumberFormatException) {
35+
internalLogger.log(
36+
InternalLogger.Level.ERROR,
37+
listOf(InternalLogger.Target.MAINTAINER),
38+
{ "Number format exception getting $key" },
39+
e
40+
)
41+
null
42+
} catch (e: RuntimeException) {
43+
internalLogger.log(
44+
InternalLogger.Level.ERROR,
45+
listOf(InternalLogger.Target.MAINTAINER),
46+
{ "Runtime exception getting $key" },
47+
e
48+
)
49+
null
50+
}
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.rum.internal.domain.accessibility
8+
9+
import android.content.Context
10+
import android.provider.Settings
11+
import android.provider.Settings.SettingNotFoundException
12+
import com.datadog.android.api.InternalLogger
13+
14+
internal class SecureWrapper {
15+
@Suppress("TooGenericExceptionCaught")
16+
internal fun getInt(
17+
internalLogger: InternalLogger,
18+
applicationContext: Context,
19+
key: String
20+
): Boolean? {
21+
return try {
22+
Settings.Secure.getInt(
23+
applicationContext.contentResolver,
24+
key,
25+
0
26+
) != 0
27+
} catch (e: SettingNotFoundException) {
28+
internalLogger.log(
29+
InternalLogger.Level.ERROR,
30+
listOf(InternalLogger.Target.MAINTAINER),
31+
{ "Setting cannot be found $key" },
32+
e
33+
)
34+
null
35+
} catch (e: SecurityException) {
36+
internalLogger.log(
37+
InternalLogger.Level.ERROR,
38+
listOf(InternalLogger.Target.MAINTAINER),
39+
{ "Security exception accessing $key" },
40+
e
41+
)
42+
null
43+
} catch (e: RuntimeException) {
44+
internalLogger.log(
45+
InternalLogger.Level.ERROR,
46+
listOf(InternalLogger.Target.MAINTAINER),
47+
{ "Runtime exception $key" },
48+
e
49+
)
50+
null
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)