Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically include accessibility stats #59

Merged
merged 7 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 80 additions & 63 deletions README.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ composeBom = "2024.04.01"
ktor = "3.0.2"
kotlinx = "1.7.3"
coroutines = "1.10.1"
uiautomator = "2.3.0"
vanniktech-publish = "0.30.0"
appcompat = "1.7.0"
mockk = "1.13.13"
Expand All @@ -19,6 +20,7 @@ androidxTest = "1.6.1"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
Expand Down
1 change: 1 addition & 0 deletions lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ dependencies {
androidTestImplementation(libs.androidx.jUnitTestRules)
androidTestImplementation(libs.androidx.ui.tooling)
androidTestImplementation(libs.androidx.ui.test.manifest)
androidTestImplementation(libs.androidx.uiautomator)

testImplementation(libs.mockk)
testImplementation(libs.mockk.android)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.telemetrydeck.sdk

import android.app.Application
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.telemetrydeck.sdk.providers.AccessibilityProvider
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class AccessibilityProviderFontScaleNormalTest() {
private fun createSut(): AccessibilityProvider {
val appContext = ApplicationProvider.getApplicationContext<Application>()
val sut = AccessibilityProvider()
sut.register(appContext,
TelemetryDeck(
configuration = TelemetryManagerConfiguration("32CB6574-6732-4238-879F-582FEBEB6536"),
providers = emptyList()
)
)
return sut
}

@get:Rule
val accessibilitySettingsRule = FontScaleTestRule(1.0f)

@Test
fun fontScaleMappingTest() {
val sut = createSut()
val result = sut.enrich("", null)
Assert.assertEquals("L", result["TelemetryDeck.Accessibility.fontScale"])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.telemetrydeck.sdk


import android.app.Application
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.telemetrydeck.sdk.providers.AccessibilityProvider
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class AccessibilityProviderEnrichmentParametersTest {
private fun createSut(): AccessibilityProvider {
val appContext = ApplicationProvider.getApplicationContext<Application>()
val sut = AccessibilityProvider()
sut.register(appContext, TelemetryDeck(configuration = TelemetryManagerConfiguration("32CB6574-6732-4238-879F-582FEBEB6536"), providers = emptyList()))
return sut
}

@Test
fun appendsAllExpectedParameters() {
val sut = createSut()
val result = sut.enrich("", null)
assertTrue(result.containsKey("TelemetryDeck.Accessibility.fontWeightAdjustment"))
assertTrue(result.containsKey("TelemetryDeck.Accessibility.isBoldTextEnabled"))
assertTrue(result.containsKey("TelemetryDeck.Accessibility.isDarkerSystemColorsEnabled"))
assertTrue(result.containsKey("TelemetryDeck.Accessibility.fontScale"))
assertTrue(result.containsKey("TelemetryDeck.Accessibility.isInvertColorsEnabled"))
assertTrue(result.containsKey("TelemetryDeck.Accessibility.shouldDifferentiateWithoutColor"))
assertTrue(result.containsKey("TelemetryDeck.Accessibility.isReduceMotionEnabled"))
assertTrue(result.containsKey("TelemetryDeck.Accessibility.isReduceTransparencyEnabled"))
assertTrue(result.containsKey("TelemetryDeck.UserPreference.layoutDirection"))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.telemetrydeck.sdk

import android.content.Context
import android.provider.Settings
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement

class FontScaleTestRule (private val fontScale: Float) : TestRule {
private val context: Context = ApplicationProvider.getApplicationContext()
private val uiDevice: UiDevice =
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
try {
setupAccessibilityOptions()
base.evaluate()
} finally {
cleanupAccessibilityOptions()
}
}
}
}

private fun setupAccessibilityOptions() {
enableLargeText()
}

private fun cleanupAccessibilityOptions() {
disableLargeText()
}

private fun enableLargeText() {
executeShellCommand("settings put system font_scale $fontScale")
waitForFontScaleChange(fontScale)
}

private fun disableLargeText() {
executeShellCommand("settings put system font_scale 1.0")
waitForFontScaleChange(1.0f)
}

private fun waitForFontScaleChange(expectedScale: Float, timeoutMillis: Long = 2000) {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < timeoutMillis) {
val currentScale = Settings.System.getFloat(
context.contentResolver,
Settings.System.FONT_SCALE,
1.0f
)
if (currentScale == expectedScale) {
return
}
Thread.sleep(100) // Wait for 100 milliseconds before checking again
}
throw AssertionError("Font scale did not change to $expectedScale within timeout")
}

private fun executeShellCommand(command: String) {
uiDevice.executeShellCommand(command)
}
}
12 changes: 12 additions & 0 deletions lib/src/main/java/com/telemetrydeck/sdk/params/Accessibility.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.telemetrydeck.sdk.params

enum class Accessibility(val paramName: String) {
FontWeightAdjustment("TelemetryDeck.Accessibility.fontWeightAdjustment"),
FontScale("TelemetryDeck.Accessibility.fontScale"),
IsBoldTextEnabled("TelemetryDeck.Accessibility.isBoldTextEnabled"),
IsDarkerSystemColorsEnabled("TelemetryDeck.Accessibility.isDarkerSystemColorsEnabled"),
IsInvertColorsEnabled("TelemetryDeck.Accessibility.isInvertColorsEnabled"),
IsReduceMotionEnabled("TelemetryDeck.Accessibility.isReduceMotionEnabled"),
IsReduceTransparencyEnabled("TelemetryDeck.Accessibility.isReduceTransparencyEnabled"),
ShouldDifferentiateWithoutColor("TelemetryDeck.Accessibility.shouldDifferentiateWithoutColor"),
}
2 changes: 1 addition & 1 deletion lib/src/main/java/com/telemetrydeck/sdk/params/Device.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ internal enum class Device(val paramName: String) {
Orientation("TelemetryDeck.Device.orientation"), // iOS compatibility note: on Android, there are additional orientations
ScreenDensity("TelemetryDeck.Device.screenDensity"),
ScreenHeight("TelemetryDeck.Device.screenResolutionHeight"),
ScreenWidth("TelemetryDeck.Device.screenResolutionWidth")
ScreenWidth("TelemetryDeck.Device.screenResolutionWidth"),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.telemetrydeck.sdk.params

enum class UserPreferences(val paramName: String) {
LayoutDirection("TelemetryDeck.UserPreference.layoutDirection"),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package com.telemetrydeck.sdk.providers

import android.app.Application
import android.content.res.Configuration
import android.os.Build
import android.provider.Settings
import android.util.LayoutDirection
import androidx.core.text.layoutDirection
import com.telemetrydeck.sdk.TelemetryDeckProvider
import com.telemetrydeck.sdk.TelemetryDeckSignalProcessor
import com.telemetrydeck.sdk.params.Accessibility
import com.telemetrydeck.sdk.params.UserPreferences
import java.lang.ref.WeakReference
import java.util.Locale


class AccessibilityProvider : TelemetryDeckProvider {
private var app: WeakReference<Application?>? = null
private var manager: WeakReference<TelemetryDeckSignalProcessor>? = null

override fun register(ctx: Application?, client: TelemetryDeckSignalProcessor) {
this.app = WeakReference(ctx)
this.manager = WeakReference(client)
}

override fun stop() {

}

override fun enrich(
signalType: String,
clientUser: String?,
additionalPayload: Map<String, String>
): Map<String, String> {
val signalPayload = additionalPayload.toMutableMap()
for (item in getConfigurationParams()) {
if (!signalPayload.containsKey(item.key)) {
signalPayload[item.key] = item.value
}
}
return signalPayload
}

private fun getConfigurationParams(): Map<String, String> {

val context = this.app?.get()?.applicationContext ?: return emptyMap()
val config = context.resources.configuration
val attributes = mutableMapOf<String, String>()

try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
attributes[Accessibility.FontWeightAdjustment.paramName] =
"${config.fontWeightAdjustment}"
attributes[Accessibility.IsBoldTextEnabled.paramName] =
"${config.fontWeightAdjustment > 0}"
}
} catch (e: Exception) {
this.manager?.get()?.debugLogger?.error("Error detecting FontWeightAdjustment: ${e.stackTraceToString()}")
}


try {
isDarkModeEnabled().let {
attributes[Accessibility.IsDarkerSystemColorsEnabled.paramName] = "$it"
}
} catch (e: Exception) {
this.manager?.get()?.debugLogger?.error("Error detecting IsDarkerSystemColorsEnabled: ${e.stackTraceToString()}")
}

try {
attributes[Accessibility.FontScale.paramName] = mapFontScaleToTelemetryValue(config.fontScale).scale
} catch (e: Exception) {
this.manager?.get()?.debugLogger?.error("Error detecting FontScale: ${e.stackTraceToString()}")
}

try {
val isColorInversionEnabled = Settings.Secure.getInt(
context.contentResolver,
Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
0 // Default value if the setting is not found
) == 1
attributes[Accessibility.IsInvertColorsEnabled.paramName] = "$isColorInversionEnabled"
} catch (e: Exception) {
this.manager?.get()?.debugLogger?.error("Error detecting IsInvertColorsEnabled: ${e.stackTraceToString()}")
}

try {
val colorCorrectionSettingKey = "accessibility_display_daltonizer_enabled"
val colorModeSettingKey = "accessibility_display_daltonizer"
val isColorCorrectionEnabled =
Settings.Secure.getInt(context.contentResolver, colorCorrectionSettingKey) == 1 &&
Settings.Secure.getInt(context.contentResolver, colorModeSettingKey) == 0
attributes[Accessibility.ShouldDifferentiateWithoutColor.paramName] =
"$isColorCorrectionEnabled"
} catch (e: Settings.SettingNotFoundException) {
attributes[Accessibility.ShouldDifferentiateWithoutColor.paramName] = "false"
} catch (e: Exception) {
this.manager?.get()?.debugLogger?.error("Error detecting ShouldDifferentiateWithoutColor: ${e.stackTraceToString()}")
}

try {
val transitionAnimationScale = Settings.Global.getFloat(
context.contentResolver,
Settings.Global.TRANSITION_ANIMATION_SCALE
)

val animatorDurationScale = Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE
)

attributes[Accessibility.IsReduceMotionEnabled.paramName] =
"${transitionAnimationScale == 0.0f && animatorDurationScale == 0.0f}"
attributes[Accessibility.IsReduceTransparencyEnabled.paramName] =
"${transitionAnimationScale == 0.0f}"
} catch (e: Exception) {
this.manager?.get()?.debugLogger?.error("Error detecting IsReduceMotionEnabled: ${e.stackTraceToString()}")
}

try {
attributes[UserPreferences.LayoutDirection.paramName] =
when (Locale.getDefault().layoutDirection == LayoutDirection.RTL) {
true -> "rightToLeft"
false -> "leftToRight"
}
} catch (e: Exception) {
this.manager?.get()?.debugLogger?.error("Error detecting LayoutDirection: ${e.stackTraceToString()}")
}

return attributes

}

private fun isDarkModeEnabled(): Boolean? {
val context = this.app?.get()?.applicationContext ?: return null
val nightModeFlags: Int =
context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK

when (nightModeFlags) {
Configuration.UI_MODE_NIGHT_YES -> {
return true
}

Configuration.UI_MODE_NIGHT_NO -> {
return false
}

Configuration.UI_MODE_NIGHT_UNDEFINED -> {
return null
}
}

return null
}


fun mapFontScaleToTelemetryValue(fontScale: Float): TelemetryValue {
// Note: font scale of 1.0 maps to TelemetryValue.L to match the scale in the SwiftSDK
when {
fontScale <= 0.8f -> return TelemetryValue.XS
fontScale > 0.8f && fontScale < 0.9f -> return TelemetryValue.S
fontScale >= 0.9f && fontScale < 1.0f -> return TelemetryValue.M
fontScale == 1.0f -> return TelemetryValue.L
fontScale >= 1.0f && fontScale < 1.3f -> return TelemetryValue.XL
fontScale >= 1.3f && fontScale < 1.4f -> return TelemetryValue.XXL
fontScale >= 1.4f && fontScale < 1.5f -> return TelemetryValue.XXXL
fontScale >= 1.5f && fontScale < 1.6f -> return TelemetryValue.AccessibilityM
fontScale >= 1.6f && fontScale < 1.7f -> return TelemetryValue.AccessibilityL
fontScale >= 1.7f && fontScale < 1.8f -> return TelemetryValue.AccessibilityXL
fontScale >= 1.8f && fontScale < 1.9f -> return TelemetryValue.AccessibilityXXL
fontScale >= 1.9f && fontScale < 2.0f -> return TelemetryValue.AccessibilityXXXL
fontScale >= 2.0f -> return TelemetryValue.AccessibilityXXXL
else -> return TelemetryValue.Unspecified
}
}

enum class TelemetryValue(val scale: String) {
Unspecified("unspecified"),
XS("XS"),
S("S"),
M("M"),
L("L"),
XL("XL"),
XXL("XXL"),
XXXL("XXXL"),
AccessibilityM("AccessibilityM"),
AccessibilityL("AccessibilityL"),
AccessibilityXL("AccessibilityXL"),
AccessibilityXXL("AccessibilityXXL"),
AccessibilityXXXL("AccessibilityXXXL"),
}
}