diff --git a/detekt_custom.yml b/detekt_custom.yml index 90f56e83eb..8951cf992e 100644 --- a/detekt_custom.yml +++ b/detekt_custom.yml @@ -137,6 +137,7 @@ datadog: - "android.graphics.drawable.LayerDrawable.getDrawable(kotlin.Int):java.lang.IndexOutOfBoundsException" - "android.net.ConnectivityManager.registerDefaultNetworkCallback(android.net.ConnectivityManager.NetworkCallback):java.lang.IllegalArgumentException,java.lang.SecurityException" - "android.net.ConnectivityManager.unregisterNetworkCallback(android.net.ConnectivityManager.NetworkCallback):java.lang.SecurityException" + - "android.provider.Settings.System.getInt(android.content.ContentResolver?, kotlin.String?):android.provider.Settings.SettingNotFoundException" - "android.util.Base64.encodeToString(kotlin.ByteArray?, kotlin.Int):java.lang.AssertionError" - "android.view.Choreographer.getInstance():java.lang.IllegalStateException" - "android.view.Choreographer.postFrameCallback():java.lang.IllegalArgumentException" @@ -389,6 +390,7 @@ datadog: - "android.net.ConnectivityManager.NetworkCallback.onLost(android.net.Network)" - "android.net.ConnectivityManager.getNetworkCapabilities(android.net.Network?)" - "android.net.NetworkCapabilities.hasTransport(kotlin.Int)" + - "android.os.BatteryManager.getIntProperty(kotlin.Int)" - "android.os.Bundle.get(kotlin.String?)" - "android.os.Bundle.getString(kotlin.String?)" - "android.os.Bundle.keySet()" @@ -411,6 +413,7 @@ datadog: - "android.os.SystemClock.elapsedRealtime()" - "android.provider.Settings.Global.getUriFor(kotlin.String?)" - "android.provider.Settings.Secure.getUriFor(kotlin.String?)" + - "android.provider.Settings.System.getUriFor(kotlin.String?)" - "android.util.Log.e(kotlin.String?, kotlin.String)" - "android.util.Log.e(kotlin.String?, kotlin.String?, kotlin.Throwable?)" - "android.util.Log.getStackTraceString(kotlin.Throwable?)" diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/Rum.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/Rum.kt index e1dbed1e09..6d6e0e6597 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/Rum.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/Rum.kt @@ -141,7 +141,9 @@ object Rum { lastInteractionIdentifier = rumFeature.lastInteractionIdentifier, slowFramesListener = rumFeature.slowFramesListener, rumSessionTypeOverride = rumFeature.configuration.rumSessionTypeOverride, - accessibilitySnapshotManager = rumFeature.accessibilitySnapshotManager + accessibilitySnapshotManager = rumFeature.accessibilitySnapshotManager, + batteryInfoProvider = rumFeature.batteryInfoProvider, + displayInfoProvider = rumFeature.displayInfoProvider ) } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt index 48520b30a6..27ec6f6fc9 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt @@ -42,13 +42,20 @@ import com.datadog.android.rum.configuration.SlowFramesConfiguration import com.datadog.android.rum.configuration.VitalsUpdateFrequency import com.datadog.android.rum.internal.anr.ANRDetectorRunnable import com.datadog.android.rum.internal.debug.UiRumDebugListener +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.internal.domain.RumDataWriter -import com.datadog.android.rum.internal.domain.accessibility.AccessibilityReader +import com.datadog.android.rum.internal.domain.accessibility.AccessibilityInfo import com.datadog.android.rum.internal.domain.accessibility.AccessibilitySnapshotManager -import com.datadog.android.rum.internal.domain.accessibility.DatadogAccessibilityReader +import com.datadog.android.rum.internal.domain.accessibility.DefaultAccessibilityReader import com.datadog.android.rum.internal.domain.accessibility.DefaultAccessibilitySnapshotManager import com.datadog.android.rum.internal.domain.accessibility.NoOpAccessibilityReader import com.datadog.android.rum.internal.domain.accessibility.NoOpAccessibilitySnapshotManager +import com.datadog.android.rum.internal.domain.battery.BatteryInfo +import com.datadog.android.rum.internal.domain.battery.DefaultBatteryInfoProvider +import com.datadog.android.rum.internal.domain.battery.NoOpBatteryInfoProvider +import com.datadog.android.rum.internal.domain.display.DefaultDisplayInfoProvider +import com.datadog.android.rum.internal.domain.display.DisplayInfo +import com.datadog.android.rum.internal.domain.display.NoOpDisplayInfoProvider import com.datadog.android.rum.internal.domain.event.RumEventMapper import com.datadog.android.rum.internal.domain.event.RumEventMetaDeserializer import com.datadog.android.rum.internal.domain.event.RumEventMetaSerializer @@ -151,9 +158,11 @@ internal class RumFeature( internal var initialResourceIdentifier: InitialResourceIdentifier = NoOpInitialResourceIdentifier() internal var lastInteractionIdentifier: LastInteractionIdentifier? = NoOpLastInteractionIdentifier() internal var slowFramesListener: SlowFramesListener? = null - internal var accessibilityReader: AccessibilityReader = NoOpAccessibilityReader() + internal var accessibilityReader: InfoProvider = NoOpAccessibilityReader() internal var accessibilitySnapshotManager: AccessibilitySnapshotManager = NoOpAccessibilitySnapshotManager() + internal var batteryInfoProvider: InfoProvider = NoOpBatteryInfoProvider() + internal var displayInfoProvider: InfoProvider = NoOpDisplayInfoProvider() private val lateCrashEventHandler by lazy { lateCrashReporterFactory(sdkCore as InternalSdkCore) } @@ -166,8 +175,10 @@ internal class RumFeature( this.appContext = appContext if (configuration.collectAccessibility) { - accessibilityReader = - DatadogAccessibilityReader(applicationContext = appContext, internalLogger = sdkCore.internalLogger) + accessibilityReader = DefaultAccessibilityReader( + internalLogger = sdkCore.internalLogger, + applicationContext = appContext + ) accessibilitySnapshotManager = DefaultAccessibilitySnapshotManager(accessibilityReader) } @@ -193,6 +204,13 @@ internal class RumFeature( telemetryConfigurationSampleRate = configuration.telemetryConfigurationSampleRate backgroundEventTracking = configuration.backgroundEventTracking trackFrustrations = configuration.trackFrustrations + batteryInfoProvider = DefaultBatteryInfoProvider( + applicationContext = appContext + ) + displayInfoProvider = DefaultDisplayInfoProvider( + applicationContext = appContext, + internalLogger = sdkCore.internalLogger + ) configuration.viewTrackingStrategy?.let { viewTrackingStrategy = it } actionTrackingStrategy = if (configuration.userActionTracking) { @@ -306,17 +324,26 @@ internal class RumFeature( vitalExecutorService = NoOpScheduledExecutorService() sessionListener = NoOpRumSessionListener() + cleanupInfoProviders() + + GlobalRumMonitor.unregister(sdkCore) + } + + // endregion + + private fun cleanupInfoProviders() { if (configuration.collectAccessibility) { accessibilityReader.cleanup() accessibilityReader = NoOpAccessibilityReader() accessibilitySnapshotManager = NoOpAccessibilitySnapshotManager() } - GlobalRumMonitor.unregister(sdkCore) + batteryInfoProvider.cleanup() + batteryInfoProvider = NoOpBatteryInfoProvider() + displayInfoProvider.cleanup() + displayInfoProvider = NoOpDisplayInfoProvider() } - // endregion - private fun createDataWriter( configuration: Configuration, sdkCore: InternalSdkCore diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/AccessibilityReader.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/InfoProvider.kt similarity index 55% rename from features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/AccessibilityReader.kt rename to features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/InfoProvider.kt index 075bcce784..f62e93040b 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/AccessibilityReader.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/InfoProvider.kt @@ -4,12 +4,11 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.rum.internal.domain.accessibility +package com.datadog.android.rum.internal.domain -import com.datadog.tools.annotation.NoOpImplementation +internal interface InfoData -@NoOpImplementation -internal interface AccessibilityReader { - fun getState(): Map +internal interface InfoProvider { + fun getState(): T fun cleanup() } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/Accessibility.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/Accessibility.kt deleted file mode 100644 index 1a966c15f3..0000000000 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/Accessibility.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.domain.accessibility - -/** - * Represents the accessibility settings state of the device. - * - * @property textSize The font scale factor (1.0 = normal, >1.0 = larger, <1.0 = smaller) - * @property isScreenReaderEnabled Whether touch exploration is enabled (TalkBack, etc.) - * @property isColorInversionEnabled Whether color inversion is enabled - * @property isClosedCaptioningEnabled Whether closed captions are enabled - * @property isReducedAnimationsEnabled Whether animations are disabled/reduced - * @property isScreenPinningEnabled Whether the device is in single-app mode - * @property isRtlEnabled Whether right to left layout is enabled - */ -internal data class Accessibility( - val textSize: String? = null, - val isScreenReaderEnabled: Boolean? = null, - val isColorInversionEnabled: Boolean? = null, - val isClosedCaptioningEnabled: Boolean? = null, - val isReducedAnimationsEnabled: Boolean? = null, - val isScreenPinningEnabled: Boolean? = null, - val isRtlEnabled: Boolean? = null -) { - fun toMap(): Map = buildMap { - textSize?.let { put(TEXT_SIZE_KEY, it) } - isScreenReaderEnabled?.let { put(SCREEN_READER_ENABLED_KEY, it) } - isColorInversionEnabled?.let { put(COLOR_INVERSION_ENABLED_KEY, it) } - isClosedCaptioningEnabled?.let { put(CLOSED_CAPTIONING_ENABLED_KEY, it) } - isReducedAnimationsEnabled?.let { put(REDUCED_ANIMATIONS_ENABLED_KEY, it) } - isScreenPinningEnabled?.let { put(SCREEN_PINNING_ENABLED_KEY, it) } - isRtlEnabled?.let { put(RTL_ENABLED, it) } - } - - companion object { - internal const val TEXT_SIZE_KEY = "text_size" - internal const val SCREEN_READER_ENABLED_KEY = "screen_reader_enabled" - internal const val COLOR_INVERSION_ENABLED_KEY = "invert_colors_enabled" - internal const val CLOSED_CAPTIONING_ENABLED_KEY = "closed_captioning_enabled" - internal const val REDUCED_ANIMATIONS_ENABLED_KEY = "reduced_animations_enabled" - internal const val SCREEN_PINNING_ENABLED_KEY = "single_app_mode_enabled" - internal const val RTL_ENABLED = "rtl_enabled" - - internal fun fromMap(map: Map): Accessibility { - return Accessibility( - textSize = map[TEXT_SIZE_KEY] as? String, - isScreenReaderEnabled = map[SCREEN_READER_ENABLED_KEY] as? Boolean, - isColorInversionEnabled = map[COLOR_INVERSION_ENABLED_KEY] as? Boolean, - isClosedCaptioningEnabled = map[CLOSED_CAPTIONING_ENABLED_KEY] as? Boolean, - isReducedAnimationsEnabled = map[REDUCED_ANIMATIONS_ENABLED_KEY] as? Boolean, - isScreenPinningEnabled = map[SCREEN_PINNING_ENABLED_KEY] as? Boolean, - isRtlEnabled = map[RTL_ENABLED] as? Boolean - ) - } - } -} diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo.kt new file mode 100644 index 0000000000..bfafd2efae --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo.kt @@ -0,0 +1,30 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.domain.accessibility + +import com.datadog.android.rum.internal.domain.InfoData + +/** + * Represents the accessibility settings state of the device. + * + * @property textSize The font scale factor (1.0 = normal, >1.0 = larger, <1.0 = smaller) + * @property isScreenReaderEnabled Whether touch exploration is enabled (TalkBack, etc.) + * @property isColorInversionEnabled Whether color inversion is enabled + * @property isClosedCaptioningEnabled Whether closed captions are enabled + * @property isReducedAnimationsEnabled Whether animations are disabled/reduced + * @property isScreenPinningEnabled Whether the device is in single-app mode + * @property isRtlEnabled Whether right to left layout is enabled + */ +internal data class AccessibilityInfo( + val textSize: String? = null, + val isScreenReaderEnabled: Boolean? = null, + val isColorInversionEnabled: Boolean? = null, + val isClosedCaptioningEnabled: Boolean? = null, + val isReducedAnimationsEnabled: Boolean? = null, + val isScreenPinningEnabled: Boolean? = null, + val isRtlEnabled: Boolean? = null +) : InfoData diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager.kt index 0f45cab544..e561129db1 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager.kt @@ -10,5 +10,5 @@ import com.datadog.tools.annotation.NoOpImplementation @NoOpImplementation internal interface AccessibilitySnapshotManager { - fun latestSnapshot(): Accessibility + fun latestSnapshot(): AccessibilityInfo } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/DatadogAccessibilityReader.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilityReader.kt similarity index 83% rename from features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/DatadogAccessibilityReader.kt rename to features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilityReader.kt index d9235fb4a7..7946aa0485 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/DatadogAccessibilityReader.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilityReader.kt @@ -22,11 +22,11 @@ import android.view.View import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener import com.datadog.android.api.InternalLogger -import java.util.concurrent.atomic.AtomicBoolean +import com.datadog.android.rum.internal.domain.InfoProvider import java.util.concurrent.atomic.AtomicLong @Suppress("TooManyFunctions") -internal class DatadogAccessibilityReader( +internal class DefaultAccessibilityReader( private val internalLogger: InternalLogger, private val applicationContext: Context, private val resources: Resources = applicationContext.resources, @@ -37,14 +37,7 @@ internal class DatadogAccessibilityReader( private val secureWrapper: SecureWrapper = SecureWrapper(), private val globalWrapper: GlobalWrapper = GlobalWrapper(), private val handler: Handler = Handler(Looper.getMainLooper()) -) : AccessibilityReader, ComponentCallbacks { - - @Volatile - private var currentState = Accessibility() - - private var lastPollTime: AtomicLong = AtomicLong(0) - - private var isInitialized = AtomicBoolean(false) +) : InfoProvider, ComponentCallbacks { private val displayInversionListener = object : ContentObserver(handler) { override fun onChange(selfChange: Boolean, uri: Uri?) { @@ -72,15 +65,22 @@ internal class DatadogAccessibilityReader( updateState { it.copy(isScreenReaderEnabled = newScreenReaderEnabled) } } + @Volatile + private var currentState = AccessibilityInfo() + + private var lastPollTime: AtomicLong = AtomicLong(0) + + init { + registerListeners() + buildInitialState() + } + override fun cleanup() { - if (isInitialized.get()) { - accessibilityManager?.removeTouchExplorationStateChangeListener(touchListener) - applicationContext.contentResolver.unregisterContentObserver(animationDurationListener) - applicationContext.contentResolver.unregisterContentObserver(captioningListener) - applicationContext.contentResolver.unregisterContentObserver(displayInversionListener) - applicationContext.unregisterComponentCallbacks(this) - isInitialized.set(false) - } + accessibilityManager?.removeTouchExplorationStateChangeListener(touchListener) + applicationContext.contentResolver.unregisterContentObserver(animationDurationListener) + applicationContext.contentResolver.unregisterContentObserver(captioningListener) + applicationContext.contentResolver.unregisterContentObserver(displayInversionListener) + applicationContext.unregisterComponentCallbacks(this) } override fun onLowMemory() { @@ -96,9 +96,7 @@ internal class DatadogAccessibilityReader( } @Synchronized - override fun getState(): Map { - ensureInitialized() - + override fun getState(): AccessibilityInfo { val currentTime = System.currentTimeMillis() val shouldPoll = currentTime - lastPollTime.get() >= POLL_THRESHOLD if (shouldPoll) { @@ -106,24 +104,16 @@ internal class DatadogAccessibilityReader( pollForAttributesWithoutListeners() } - return currentState.toMap() + return currentState } @Synchronized - private fun updateState(updater: (Accessibility) -> Accessibility) { + private fun updateState(updater: (AccessibilityInfo) -> AccessibilityInfo) { currentState = updater(currentState) } - private fun ensureInitialized() { - if (!isInitialized.get()) { - registerListeners() - currentState = buildInitialState() - isInitialized.set(true) - } - } - - private fun buildInitialState(): Accessibility { - return Accessibility( + private fun buildInitialState() { + currentState = AccessibilityInfo( textSize = getTextSize(), isScreenReaderEnabled = isScreenReaderEnabled(accessibilityManager), isColorInversionEnabled = isDisplayInversionEnabled(), @@ -159,11 +149,11 @@ internal class DatadogAccessibilityReader( } private fun isDisplayInversionEnabled(): Boolean? { - return getSecureInt(ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) + return isSettingEnabled(ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) } private fun isClosedCaptioningEnabled(): Boolean? { - return getSecureInt(CAPTIONING_ENABLED_KEY) + return isSettingEnabled(CAPTIONING_ENABLED_KEY) } private fun isLockToScreenEnabled(): Boolean? { @@ -187,12 +177,14 @@ internal class DatadogAccessibilityReader( } } - private fun getSecureInt(key: String): Boolean? { + private fun isSettingEnabled(key: String): Boolean? { return secureWrapper.getInt( internalLogger = internalLogger, applicationContext = applicationContext, key = key - ) + )?.let { + it == 1 + } } private fun getTextSize(): String { diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilitySnapshotManager.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilitySnapshotManager.kt index 30c9e9da47..1d953a9696 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilitySnapshotManager.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilitySnapshotManager.kt @@ -6,29 +6,71 @@ package com.datadog.android.rum.internal.domain.accessibility +import com.datadog.android.rum.internal.domain.InfoProvider + internal class DefaultAccessibilitySnapshotManager( - private val accessibilityReader: AccessibilityReader + private val accessibilityReader: InfoProvider ) : AccessibilitySnapshotManager { - private val lastSnapshot = mutableMapOf() + @Volatile + private var lastSnapshot = AccessibilityInfo() @Synchronized - override fun latestSnapshot(): Accessibility { - val newAccessibilityState = accessibilityReader.getState() - val newSnapshot = mutableMapOf() + override fun latestSnapshot(): AccessibilityInfo { + val newAccessibilitySnapshot = accessibilityReader.getState() + + val deltaSnapshot = AccessibilityInfo( + textSize = + if (newAccessibilitySnapshot.textSize != lastSnapshot.textSize) { + newAccessibilitySnapshot.textSize + } else { + null + }, + + isScreenReaderEnabled = + if (newAccessibilitySnapshot.isScreenReaderEnabled != lastSnapshot.isScreenReaderEnabled) { + newAccessibilitySnapshot.isScreenReaderEnabled + } else { + null + }, - // remove the ones we saw already - for (key in newAccessibilityState.keys) { - val newValue = newAccessibilityState[key] - ?: continue + isColorInversionEnabled = + if (newAccessibilitySnapshot.isColorInversionEnabled != lastSnapshot.isColorInversionEnabled) { + newAccessibilitySnapshot.isColorInversionEnabled + } else { + null + }, - val oldValue = lastSnapshot[key] + isClosedCaptioningEnabled = + if (newAccessibilitySnapshot.isClosedCaptioningEnabled != lastSnapshot.isClosedCaptioningEnabled) { + newAccessibilitySnapshot.isClosedCaptioningEnabled + } else { + null + }, - if (newValue != oldValue) { - newSnapshot[key] = newValue - lastSnapshot[key] = newValue + isReducedAnimationsEnabled = + if (newAccessibilitySnapshot.isReducedAnimationsEnabled != lastSnapshot.isReducedAnimationsEnabled) { + newAccessibilitySnapshot.isReducedAnimationsEnabled + } else { + null + }, + + isScreenPinningEnabled = + if (newAccessibilitySnapshot.isScreenPinningEnabled != lastSnapshot.isScreenPinningEnabled) { + newAccessibilitySnapshot.isScreenPinningEnabled + } else { + null + }, + + isRtlEnabled = + if (newAccessibilitySnapshot.isRtlEnabled != lastSnapshot.isRtlEnabled) { + newAccessibilitySnapshot.isRtlEnabled + } else { + null } - } + ) + + lastSnapshot = newAccessibilitySnapshot - return Accessibility.fromMap(newSnapshot) + return deltaSnapshot } } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/GlobalWrapper.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/GlobalWrapper.kt index f3963bb8c4..301ed61776 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/GlobalWrapper.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/GlobalWrapper.kt @@ -12,7 +12,7 @@ import android.provider.Settings.SettingNotFoundException import com.datadog.android.api.InternalLogger internal class GlobalWrapper { - @Suppress("TooGenericExceptionCaught", "UnsafeThirdPartyFunctionCall") // exceptions caught + @Suppress("UnsafeThirdPartyFunctionCall") internal fun getFloat( internalLogger: InternalLogger, applicationContext: Context, @@ -31,22 +31,6 @@ internal class GlobalWrapper { e ) null - } catch (e: NumberFormatException) { - internalLogger.log( - InternalLogger.Level.ERROR, - listOf(InternalLogger.Target.MAINTAINER), - { "Number format exception getting $key" }, - e - ) - null - } catch (e: RuntimeException) { - internalLogger.log( - InternalLogger.Level.ERROR, - listOf(InternalLogger.Target.MAINTAINER), - { "Runtime exception getting $key" }, - e - ) - null } } } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilityReader.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilityReader.kt new file mode 100644 index 0000000000..68c059160c --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilityReader.kt @@ -0,0 +1,17 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.domain.accessibility + +import com.datadog.android.rum.internal.domain.InfoProvider + +internal class NoOpAccessibilityReader : InfoProvider { + override fun getState(): AccessibilityInfo { + return AccessibilityInfo() + } + + override fun cleanup() {} +} diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/SecureWrapper.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/SecureWrapper.kt index 8cba255e7d..3085b50065 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/SecureWrapper.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/accessibility/SecureWrapper.kt @@ -12,18 +12,19 @@ import android.provider.Settings.SettingNotFoundException import com.datadog.android.api.InternalLogger internal class SecureWrapper { - @Suppress("TooGenericExceptionCaught", "UnsafeThirdPartyFunctionCall") // exceptions caught + @Suppress("UnsafeThirdPartyFunctionCall") internal fun getInt( internalLogger: InternalLogger, applicationContext: Context, key: String - ): Boolean? { + ): Int? { + // returns -1 if unable to retrieve the key return try { Settings.Secure.getInt( applicationContext.contentResolver, key, - 0 - ) != 0 + -1 + ) } catch (e: SettingNotFoundException) { internalLogger.log( InternalLogger.Level.ERROR, @@ -31,7 +32,7 @@ internal class SecureWrapper { { "Setting cannot be found $key" }, e ) - null + -1 } catch (e: SecurityException) { internalLogger.log( InternalLogger.Level.ERROR, @@ -39,15 +40,7 @@ internal class SecureWrapper { { "Security exception accessing $key" }, e ) - null - } catch (e: RuntimeException) { - internalLogger.log( - InternalLogger.Level.ERROR, - listOf(InternalLogger.Target.MAINTAINER), - { "Runtime exception $key" }, - e - ) - null + -1 } } } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/battery/BatteryInfo.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/battery/BatteryInfo.kt new file mode 100644 index 0000000000..1f8bca1ea2 --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/battery/BatteryInfo.kt @@ -0,0 +1,22 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.domain.battery + +import androidx.annotation.FloatRange +import com.datadog.android.rum.internal.domain.InfoData + +/** + * Provides information about the battery state. + * + * @property batteryLevel the current battery charge level, expressed as a float from 0.0f (empty) to 1.0f (full) + * to two decimal places. + * @property lowPowerMode a boolean indicating whether the device is currently in Low Power Mode. + */ +internal data class BatteryInfo( + @FloatRange(0.0, 1.0) val batteryLevel: Float? = null, + val lowPowerMode: Boolean? = null +) : InfoData diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/battery/DefaultBatteryInfoProvider.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/battery/DefaultBatteryInfoProvider.kt new file mode 100644 index 0000000000..9b60314d58 --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/battery/DefaultBatteryInfoProvider.kt @@ -0,0 +1,130 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.domain.battery + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Context.BATTERY_SERVICE +import android.content.Context.POWER_SERVICE +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import android.os.BatteryManager.BATTERY_PROPERTY_CAPACITY +import android.os.Build +import android.os.PowerManager +import android.os.PowerManager.ACTION_POWER_SAVE_MODE_CHANGED +import androidx.annotation.FloatRange +import com.datadog.android.rum.internal.domain.InfoProvider +import java.util.concurrent.atomic.AtomicLong + +internal class DefaultBatteryInfoProvider( + private val applicationContext: Context, + private val powerManager: PowerManager? = + applicationContext.getSystemService(POWER_SERVICE) as? PowerManager, + private val batteryManager: BatteryManager? = applicationContext.getSystemService( + BATTERY_SERVICE + ) as? BatteryManager, + private val batteryLevelPollInterval: Int = BATTERY_POLL_INTERVAL_MS, + private val systemClockWrapper: SystemClockWrapper = SystemClockWrapper() // this wrapper is needed for unit tests +) : InfoProvider { + + @Volatile + @FloatRange(0.0, 1.0) + var batteryLevel: Float? = null + + @Volatile + var lowPowerMode: Boolean? = null + + private var lastTimeBatteryLevelChecked = AtomicLong(systemClockWrapper.elapsedRealTime()) + + private val powerSaveModeReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val isPowerSaveMode = powerManager?.isPowerSaveMode + isPowerSaveMode?.let { + lowPowerMode = it + } + } + } + + init { + registerReceivers() + buildInitialState() + } + + @Synchronized + override fun getState(): BatteryInfo { + // while we could register a receiver for battery level, + // it fires far too often (multiple times per second) + // so it seems better to only poll battery charge state once in a period of time + val now = systemClockWrapper.elapsedRealTime() + if (now - batteryLevelPollInterval >= lastTimeBatteryLevelChecked.get()) { + lastTimeBatteryLevelChecked.set(now) + + resolveBatteryLevel()?.let { + batteryLevel = it + } + } + + // construct current state from the latest values + return BatteryInfo( + batteryLevel = batteryLevel, + lowPowerMode = lowPowerMode + ) + } + + override fun cleanup() { + safeUnregisterReceiver(powerSaveModeReceiver) + } + + private fun safeUnregisterReceiver(receiver: BroadcastReceiver) { + try { + applicationContext.unregisterReceiver(receiver) + } catch (_: IllegalArgumentException) { + // ignore - receiver was not previously registered or already unregistered + } + } + + private fun buildInitialState() { + lowPowerMode = resolveLowPowerMode() + batteryLevel = resolveBatteryLevel() + } + + private fun registerReceivers() { + val powerSaveFilter = IntentFilter(ACTION_POWER_SAVE_MODE_CHANGED) + applicationContext.registerReceiver(powerSaveModeReceiver, powerSaveFilter) + } + + private fun resolveLowPowerMode(): Boolean? { + return powerManager?.isPowerSaveMode + } + + private fun resolveBatteryLevel(): Float? { + val batteryLevel = batteryManager?.getIntProperty(BATTERY_PROPERTY_CAPACITY) + + return batteryLevel?.let { + // if there was a problem retrieving the capacity + val retrievalFailureCode = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + Integer.MIN_VALUE + } else { + 0 + } + + if (it == retrievalFailureCode) return@let null + + normalizeBatteryLevel(it) + } + } + + private fun normalizeBatteryLevel(batteryLevel: Int): Float { + return batteryLevel / FULL_BATTERY_PCT + } + + private companion object { + const val FULL_BATTERY_PCT = 100f + const val BATTERY_POLL_INTERVAL_MS = 60_000 // 60 seconds + } +} diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/battery/NoOpBatteryInfoProvider.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/battery/NoOpBatteryInfoProvider.kt new file mode 100644 index 0000000000..322c6aac14 --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/battery/NoOpBatteryInfoProvider.kt @@ -0,0 +1,17 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.domain.battery + +import com.datadog.android.rum.internal.domain.InfoProvider + +internal class NoOpBatteryInfoProvider : InfoProvider { + override fun getState(): BatteryInfo { + return BatteryInfo() + } + + override fun cleanup() {} +} diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/battery/SystemClockWrapper.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/battery/SystemClockWrapper.kt new file mode 100644 index 0000000000..2fbe393cbb --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/battery/SystemClockWrapper.kt @@ -0,0 +1,15 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.domain.battery + +import android.os.SystemClock + +internal class SystemClockWrapper { + fun elapsedRealTime(): Long { + return SystemClock.elapsedRealtime() + } +} diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/display/DefaultDisplayInfoProvider.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/display/DefaultDisplayInfoProvider.kt new file mode 100644 index 0000000000..76c5c31b7f --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/display/DefaultDisplayInfoProvider.kt @@ -0,0 +1,93 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.domain.display + +import android.content.ContentResolver +import android.content.Context +import android.database.ContentObserver +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.provider.Settings.System.SCREEN_BRIGHTNESS +import com.datadog.android.api.InternalLogger +import com.datadog.android.rum.internal.domain.InfoProvider +import kotlin.math.roundToInt + +internal class DefaultDisplayInfoProvider( + private val applicationContext: Context, + private val internalLogger: InternalLogger, + private val systemSettingsWrapper: SystemSettingsWrapper = SystemSettingsWrapper( + applicationContext = applicationContext, + internalLogger = internalLogger + ), + private val contentResolver: ContentResolver = applicationContext.contentResolver, + private val handler: Handler = Handler(Looper.getMainLooper()) +) : InfoProvider { + + @Volatile + private var currentState = DisplayInfo() + + private val brightnessObserver = object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean) { + val brightness = systemSettingsWrapper.getInt(SCREEN_BRIGHTNESS) + + if (brightness != Integer.MIN_VALUE) { + val normalizedValue = normalizeValue(brightness) + + currentState = currentState.copy( + screenBrightness = normalizedValue + ) + } + } + } + + init { + registerListeners() + buildInitialState() + } + + override fun getState(): DisplayInfo { + return currentState + } + + override fun cleanup() { + contentResolver.unregisterContentObserver(brightnessObserver) + } + + private fun registerListeners() { + val brightnessUri = Settings.System.getUriFor(SCREEN_BRIGHTNESS) + brightnessUri?.let { + contentResolver.registerContentObserver(brightnessUri, false, brightnessObserver) + } + } + + private fun buildInitialState() { + val brightness = systemSettingsWrapper.getInt(SCREEN_BRIGHTNESS) + + // if we got a valid value + if (brightness != Integer.MIN_VALUE) { + val normalizedValue = normalizeValue(brightness) + currentState = DisplayInfo( + screenBrightness = normalizedValue + ) + } + } + + private fun normalizeValue(value: Int): Float { + val normalizedBrightness = value / MAX_BRIGHTNESS + return roundToOneDecimalPlace(normalizedBrightness) + } + + private fun roundToOneDecimalPlace(input: Float): Float { + return (input * DECIMAL_SCALING).roundToInt() / DECIMAL_SCALING + } + + private companion object { + const val MAX_BRIGHTNESS = 255f + const val DECIMAL_SCALING = 10f + } +} diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/display/DisplayInfo.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/display/DisplayInfo.kt new file mode 100644 index 0000000000..bdda217359 --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/display/DisplayInfo.kt @@ -0,0 +1,19 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.domain.display + +import com.datadog.android.rum.internal.domain.InfoData + +/** + * Provides information about the display state. + * + * @property screenBrightness The current screen brightness, + * normalized as a float between 0.0 (darkest) and 1.0 (brightest). + */ +internal data class DisplayInfo( + val screenBrightness: Number? = null +) : InfoData diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/display/NoOpDisplayInfoProvider.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/display/NoOpDisplayInfoProvider.kt new file mode 100644 index 0000000000..5c773340ea --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/display/NoOpDisplayInfoProvider.kt @@ -0,0 +1,17 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.domain.display + +import com.datadog.android.rum.internal.domain.InfoProvider + +internal class NoOpDisplayInfoProvider : InfoProvider { + override fun getState(): DisplayInfo { + return DisplayInfo() + } + + override fun cleanup() {} +} diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/display/SystemSettingsWrapper.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/display/SystemSettingsWrapper.kt new file mode 100644 index 0000000000..d09d992330 --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/display/SystemSettingsWrapper.kt @@ -0,0 +1,30 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.domain.display + +import android.content.Context +import android.provider.Settings +import com.datadog.android.api.InternalLogger + +internal class SystemSettingsWrapper( + private val applicationContext: Context, + private val internalLogger: InternalLogger +) { + fun getInt(name: String): Int { + return try { + Settings.System.getInt(applicationContext.contentResolver, name) + } catch (e: Settings.SettingNotFoundException) { + internalLogger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.WARN, + messageBuilder = { "Problem retrieving system value for $name" }, + throwable = e + ) + Integer.MIN_VALUE + } + } +} diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt index 876d2c5d5c..f972e6b3df 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt @@ -16,9 +16,12 @@ import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum.RumSessionType +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.domain.accessibility.AccessibilitySnapshotManager +import com.datadog.android.rum.internal.domain.battery.BatteryInfo +import com.datadog.android.rum.internal.domain.display.DisplayInfo import com.datadog.android.rum.internal.metric.SessionMetricDispatcher import com.datadog.android.rum.internal.metric.slowframes.SlowFramesListener import com.datadog.android.rum.internal.vitals.VitalMonitor @@ -43,7 +46,9 @@ internal class RumApplicationScope( internal val lastInteractionIdentifier: LastInteractionIdentifier?, private val slowFramesListener: SlowFramesListener?, private val rumSessionTypeOverride: RumSessionType?, - private val accessibilitySnapshotManager: AccessibilitySnapshotManager + private val accessibilitySnapshotManager: AccessibilitySnapshotManager, + private val batteryInfoProvider: InfoProvider, + private val displayInfoProvider: InfoProvider ) : RumScope, RumViewChangedListener { private var rumContext = RumContext(applicationId = applicationId) @@ -67,7 +72,9 @@ internal class RumApplicationScope( lastInteractionIdentifier = lastInteractionIdentifier, slowFramesListener = slowFramesListener, rumSessionTypeOverride = rumSessionTypeOverride, - accessibilitySnapshotManager = accessibilitySnapshotManager + accessibilitySnapshotManager = accessibilitySnapshotManager, + batteryInfoProvider = batteryInfoProvider, + displayInfoProvider = displayInfoProvider ) ) @@ -164,7 +171,9 @@ internal class RumApplicationScope( lastInteractionIdentifier = lastInteractionIdentifier, slowFramesListener = slowFramesListener, rumSessionTypeOverride = rumSessionTypeOverride, - accessibilitySnapshotManager = accessibilitySnapshotManager + accessibilitySnapshotManager = accessibilitySnapshotManager, + batteryInfoProvider = batteryInfoProvider, + displayInfoProvider = displayInfoProvider ) childScopes.add(newSession) if (event !is RumRawEvent.StartView) { diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt index 9806417097..cb20947d86 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt @@ -14,9 +14,12 @@ import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum.RumSessionType +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.domain.accessibility.AccessibilitySnapshotManager +import com.datadog.android.rum.internal.domain.battery.BatteryInfo +import com.datadog.android.rum.internal.domain.display.DisplayInfo import com.datadog.android.rum.internal.metric.SessionMetricDispatcher import com.datadog.android.rum.internal.metric.slowframes.SlowFramesListener import com.datadog.android.rum.internal.utils.percent @@ -47,6 +50,8 @@ internal class RumSessionScope( lastInteractionIdentifier: LastInteractionIdentifier?, slowFramesListener: SlowFramesListener?, private val accessibilitySnapshotManager: AccessibilitySnapshotManager, + private val batteryInfoProvider: InfoProvider, + private val displayInfoProvider: InfoProvider, private val sessionInactivityNanos: Long = DEFAULT_SESSION_INACTIVITY_NS, private val sessionMaxDurationNanos: Long = DEFAULT_SESSION_MAX_DURATION_NS, rumSessionTypeOverride: RumSessionType? @@ -82,7 +87,9 @@ internal class RumSessionScope( slowFramesListener = slowFramesListener, lastInteractionIdentifier = lastInteractionIdentifier, rumSessionTypeOverride = rumSessionTypeOverride, - accessibilitySnapshotManager = accessibilitySnapshotManager + accessibilitySnapshotManager = accessibilitySnapshotManager, + batteryInfoProvider = batteryInfoProvider, + displayInfoProvider = displayInfoProvider ) init { diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScope.kt index 4d0233bed0..f7d4856a5d 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScope.kt @@ -17,9 +17,12 @@ import com.datadog.android.internal.telemetry.InternalTelemetryEvent import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.RumSessionType import com.datadog.android.rum.internal.anr.ANRException +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.domain.accessibility.AccessibilitySnapshotManager +import com.datadog.android.rum.internal.domain.battery.BatteryInfo +import com.datadog.android.rum.internal.domain.display.DisplayInfo import com.datadog.android.rum.internal.metric.SessionEndedMetric import com.datadog.android.rum.internal.metric.SessionMetricDispatcher import com.datadog.android.rum.internal.metric.ViewEndedMetricDispatcher @@ -51,7 +54,9 @@ internal class RumViewManagerScope( private val slowFramesListener: SlowFramesListener?, lastInteractionIdentifier: LastInteractionIdentifier?, private val rumSessionTypeOverride: RumSessionType?, - private val accessibilitySnapshotManager: AccessibilitySnapshotManager + private val accessibilitySnapshotManager: AccessibilitySnapshotManager, + private val batteryInfoProvider: InfoProvider, + private val displayInfoProvider: InfoProvider ) : RumScope { private val interactionToNextViewMetricResolver: InteractionToNextViewMetricResolver = @@ -239,7 +244,9 @@ internal class RumViewManagerScope( networkSettledResourceIdentifier = initialResourceIdentifier, slowFramesListener = slowFramesListener, rumSessionTypeOverride = rumSessionTypeOverride, - accessibilitySnapshotManager = accessibilitySnapshotManager + accessibilitySnapshotManager = accessibilitySnapshotManager, + batteryInfoProvider = batteryInfoProvider, + displayInfoProvider = displayInfoProvider ) applicationDisplayed = true childrenScopes.add(viewScope) @@ -318,7 +325,9 @@ internal class RumViewManagerScope( viewEndedMetricDispatcher = viewEndedMetricDispatcher, slowFramesListener = slowFramesListener, rumSessionTypeOverride = rumSessionTypeOverride, - accessibilitySnapshotManager = accessibilitySnapshotManager + accessibilitySnapshotManager = accessibilitySnapshotManager, + batteryInfoProvider = batteryInfoProvider, + displayInfoProvider = displayInfoProvider ) } @@ -358,7 +367,9 @@ internal class RumViewManagerScope( viewEndedMetricDispatcher = viewEndedMetricDispatcher, slowFramesListener = slowFramesListener, rumSessionTypeOverride = rumSessionTypeOverride, - accessibilitySnapshotManager = accessibilitySnapshotManager + accessibilitySnapshotManager = accessibilitySnapshotManager, + batteryInfoProvider = batteryInfoProvider, + displayInfoProvider = displayInfoProvider ) } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt index 51b65b4249..cf44fa2603 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt @@ -24,9 +24,12 @@ import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum.RumSessionType import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.anr.ANRException +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.domain.accessibility.AccessibilitySnapshotManager +import com.datadog.android.rum.internal.domain.battery.BatteryInfo +import com.datadog.android.rum.internal.domain.display.DisplayInfo import com.datadog.android.rum.internal.metric.NoValueReason import com.datadog.android.rum.internal.metric.SessionMetricDispatcher import com.datadog.android.rum.internal.metric.ViewEndedMetricDispatcher @@ -80,7 +83,9 @@ internal open class RumViewScope( private val slowFramesListener: SlowFramesListener?, private val viewEndedMetricDispatcher: ViewMetricDispatcher, private val rumSessionTypeOverride: RumSessionType?, - private val accessibilitySnapshotManager: AccessibilitySnapshotManager + private val accessibilitySnapshotManager: AccessibilitySnapshotManager, + private val batteryInfoProvider: InfoProvider, + private val displayInfoProvider: InfoProvider ) : RumScope { internal val url = key.url.replace('.', '/') @@ -271,7 +276,9 @@ internal open class RumViewScope( viewEndedMetricDispatcher = viewEndedMetricDispatcher, slowFramesListener = slowFramesListener, rumSessionTypeOverride = rumSessionTypeOverride, - accessibilitySnapshotManager = accessibilitySnapshotManager + accessibilitySnapshotManager = accessibilitySnapshotManager, + batteryInfoProvider = batteryInfoProvider, + displayInfoProvider = displayInfoProvider ) } @@ -495,6 +502,9 @@ internal open class RumViewScope( val eventFeatureFlags = featureFlags.toMutableMap() val eventType = if (isFatal) EventType.CRASH else EventType.DEFAULT + val batteryInfo = batteryInfoProvider.getState() + val displayInfo = displayInfoProvider.getState() + sdkCore.newRumEventWriteOperation(writer, eventType) { datadogContext -> val user = datadogContext.userInfo @@ -594,7 +604,10 @@ internal open class RumViewScope( brand = datadogContext.deviceInfo.deviceBrand, architecture = datadogContext.deviceInfo.architecture, locales = datadogContext.deviceInfo.localeInfo.locales, - timeZone = datadogContext.deviceInfo.localeInfo.timeZone + timeZone = datadogContext.deviceInfo.localeInfo.timeZone, + batteryLevel = batteryInfo.batteryLevel, + powerSavingMode = batteryInfo.lowPowerMode, + brightnessLevel = displayInfo.screenBrightness ), context = ErrorEvent.Context(additionalProperties = updatedAttributes), dd = ErrorEvent.Dd( @@ -995,6 +1008,9 @@ internal open class RumViewScope( rtlEnabled = accessibilityState.isRtlEnabled ) + val batteryInfo = batteryInfoProvider.getState() + val displayInfo = displayInfoProvider.getState() + val performance = (internalAttributes[RumAttributes.FLUTTER_FIRST_BUILD_COMPLETE] as? Number)?.let { ViewEvent.Performance( fbc = ViewEvent.Fbc( @@ -1121,7 +1137,10 @@ internal open class RumViewScope( brand = datadogContext.deviceInfo.deviceBrand, architecture = datadogContext.deviceInfo.architecture, locales = datadogContext.deviceInfo.localeInfo.locales, - timeZone = datadogContext.deviceInfo.localeInfo.timeZone + timeZone = datadogContext.deviceInfo.localeInfo.timeZone, + batteryLevel = batteryInfo.batteryLevel, + powerSavingMode = batteryInfo.lowPowerMode, + brightnessLevel = displayInfo.screenBrightness ), context = ViewEvent.Context(additionalProperties = eventAdditionalAttributes), dd = ViewEvent.Dd( @@ -1557,7 +1576,9 @@ internal open class RumViewScope( networkSettledResourceIdentifier: InitialResourceIdentifier, slowFramesListener: SlowFramesListener?, rumSessionTypeOverride: RumSessionType?, - accessibilitySnapshotManager: AccessibilitySnapshotManager + accessibilitySnapshotManager: AccessibilitySnapshotManager, + batteryInfoProvider: InfoProvider, + displayInfoProvider: InfoProvider ): RumViewScope { val networkSettledMetricResolver = NetworkSettledMetricResolver( networkSettledResourceIdentifier, @@ -1591,7 +1612,9 @@ internal open class RumViewScope( viewEndedMetricDispatcher = viewEndedMetricDispatcher, slowFramesListener = slowFramesListener, rumSessionTypeOverride = rumSessionTypeOverride, - accessibilitySnapshotManager = accessibilitySnapshotManager + accessibilitySnapshotManager = accessibilitySnapshotManager, + batteryInfoProvider = batteryInfoProvider, + displayInfoProvider = displayInfoProvider ) } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt index 8f89f53abc..d0e5299f7a 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt @@ -37,10 +37,13 @@ import com.datadog.android.rum.internal.CombinedRumSessionListener import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.internal.RumFeature import com.datadog.android.rum.internal.debug.RumDebugListener +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.domain.accessibility.AccessibilitySnapshotManager import com.datadog.android.rum.internal.domain.asTime +import com.datadog.android.rum.internal.domain.battery.BatteryInfo +import com.datadog.android.rum.internal.domain.display.DisplayInfo import com.datadog.android.rum.internal.domain.event.ResourceTiming import com.datadog.android.rum.internal.domain.scope.RumApplicationScope import com.datadog.android.rum.internal.domain.scope.RumRawEvent @@ -83,7 +86,9 @@ internal class DatadogRumMonitor( lastInteractionIdentifier: LastInteractionIdentifier?, slowFramesListener: SlowFramesListener?, rumSessionTypeOverride: RumSessionType?, - accessibilitySnapshotManager: AccessibilitySnapshotManager + accessibilitySnapshotManager: AccessibilitySnapshotManager, + batteryInfoProvider: InfoProvider, + displayInfoProvider: InfoProvider ) : RumMonitor, AdvancedRumMonitor { internal var rootScope: RumScope = RumApplicationScope( @@ -102,7 +107,9 @@ internal class DatadogRumMonitor( lastInteractionIdentifier = lastInteractionIdentifier, slowFramesListener = slowFramesListener, rumSessionTypeOverride = rumSessionTypeOverride, - accessibilitySnapshotManager = accessibilitySnapshotManager + accessibilitySnapshotManager = accessibilitySnapshotManager, + batteryInfoProvider = batteryInfoProvider, + displayInfoProvider = displayInfoProvider ) internal val keepAliveRunnable = Runnable { diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumTest.kt index 072fa26903..c04a9868c2 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumTest.kt @@ -83,6 +83,8 @@ internal class RumTest { appContext = mock { whenever(it.packageName) doReturn fakePackageName whenever(it.resources) doReturn mock() + whenever(it.contentResolver) doReturn mock() + whenever(it.resources.configuration) doReturn mock() } ) assertThat(lastValue.sampleRate) @@ -132,6 +134,8 @@ internal class RumTest { appContext = mock { whenever(it.packageName) doReturn fakePackageName whenever(it.resources) doReturn mock() + whenever(it.contentResolver) doReturn mock() + whenever(it.resources.configuration) doReturn mock() } ) } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/RumFeatureTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/RumFeatureTest.kt index c9311a1c25..2cff265416 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/RumFeatureTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/RumFeatureTest.kt @@ -10,7 +10,9 @@ import android.app.Activity import android.app.ActivityManager import android.app.Application import android.app.ApplicationExitInfo +import android.content.ContentResolver import android.content.Context +import android.content.res.Resources import android.os.Build import com.datadog.android.api.InternalLogger import com.datadog.android.api.storage.NoOpDataWriter @@ -27,11 +29,14 @@ import com.datadog.android.rum.assertj.RumFeatureAssert import com.datadog.android.rum.configuration.VitalsUpdateFrequency import com.datadog.android.rum.internal.RumFeature.Companion.SLOW_FRAMES_MONITORING_DISABLED_MESSAGE import com.datadog.android.rum.internal.RumFeature.Companion.SLOW_FRAMES_MONITORING_ENABLED_MESSAGE +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.internal.domain.RumDataWriter -import com.datadog.android.rum.internal.domain.accessibility.DatadogAccessibilityReader +import com.datadog.android.rum.internal.domain.accessibility.DefaultAccessibilityReader import com.datadog.android.rum.internal.domain.accessibility.DefaultAccessibilitySnapshotManager import com.datadog.android.rum.internal.domain.accessibility.NoOpAccessibilityReader import com.datadog.android.rum.internal.domain.accessibility.NoOpAccessibilitySnapshotManager +import com.datadog.android.rum.internal.domain.battery.DefaultBatteryInfoProvider +import com.datadog.android.rum.internal.domain.display.DefaultDisplayInfoProvider import com.datadog.android.rum.internal.domain.event.RumEventMapper import com.datadog.android.rum.internal.metric.slowframes.SlowFramesListener import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor @@ -88,6 +93,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doNothing import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq @@ -144,6 +150,15 @@ internal class RumFeatureTest { whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger whenever(mockSdkCore.createScheduledExecutorService(any())) doReturn mockScheduledExecutorService + val mockContentResolver = mock() + whenever(appContext.mockInstance.contentResolver) doReturn mockContentResolver + doNothing().whenever(appContext.mockInstance).registerComponentCallbacks(any()) + doNothing().whenever(appContext.mockInstance).unregisterComponentCallbacks(any()) + + val mockResources = mock() + whenever(appContext.mockInstance.resources) doReturn mockResources + whenever(mockResources.configuration) doReturn mock() + testedFeature = RumFeature( mockSdkCore, fakeApplicationId.toString(), @@ -1381,7 +1396,7 @@ internal class RumFeatureTest { // endregion - // region accessibility + // region infoProviders @Test fun `M have noop accessibility classes W onInitialize { collectAccessibility not set or explicit false }`() { @@ -1409,13 +1424,13 @@ internal class RumFeatureTest { @Test fun `M set accessibility classes to implementation W onInitialize { collectAccessibility set true }`() { // Given - val configWithoutAccessibility = fakeConfiguration.copy( + val configWithAccessibility = fakeConfiguration.copy( collectAccessibility = true ) testedFeature = RumFeature( mockSdkCore, fakeApplicationId.toString(), - configWithoutAccessibility, + configWithAccessibility, lateCrashReporterFactory = { mockLateCrashReporter } ) @@ -1423,7 +1438,7 @@ internal class RumFeatureTest { testedFeature.onInitialize(appContext.mockInstance) // Then - assertThat(testedFeature.accessibilityReader).isInstanceOf(DatadogAccessibilityReader::class.java) + assertThat(testedFeature.accessibilityReader).isInstanceOf(DefaultAccessibilityReader::class.java) assertThat( testedFeature.accessibilitySnapshotManager ).isInstanceOf(DefaultAccessibilitySnapshotManager::class.java) @@ -1454,6 +1469,48 @@ internal class RumFeatureTest { ).isInstanceOf(NoOpAccessibilitySnapshotManager::class.java) } + @Test + fun `M setup BatteryInfoProvider W onInitialize`() { + // When + testedFeature.onInitialize(appContext.mockInstance) + + // Then + assertThat(testedFeature.batteryInfoProvider).isInstanceOf(DefaultBatteryInfoProvider::class.java) + } + + @Test + fun `M cleanup BatteryInfoProvider W onStop`() { + // Given + testedFeature.onInitialize(appContext.mockInstance) + + // When + testedFeature.onStop() + + // Then + assertThat(testedFeature.batteryInfoProvider).isInstanceOf(InfoProvider::class.java) + } + + @Test + fun `M setup DisplayInfoProvider W onInitialize`() { + // When + testedFeature.onInitialize(appContext.mockInstance) + + // Then + assertThat(testedFeature.displayInfoProvider).isInstanceOf(DefaultDisplayInfoProvider::class.java) + } + + @Test + fun `M cleanup DisplayInfoProvider W onStop`() { + // Given + testedFeature.onInitialize(appContext.mockInstance) + + // When + testedFeature.onStop() + + // Then + assertThat(testedFeature.displayInfoProvider).isInstanceOf(InfoProvider::class.java) + } + // endregion private fun verifyFrameStateAggregatorInitialized( diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/attribute/AccessibilityTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/attribute/AccessibilityTest.kt deleted file mode 100644 index e28f7478bc..0000000000 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/attribute/AccessibilityTest.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.rum.internal.attribute - -import com.datadog.android.rum.internal.domain.accessibility.Accessibility -import com.datadog.android.rum.utils.forge.Configurator -import fr.xgouchet.elmyr.annotation.BoolForgery -import fr.xgouchet.elmyr.annotation.FloatForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(ForgeExtension::class) -@ForgeConfiguration(Configurator::class) -internal class AccessibilityTest { - - @Test - fun `M return empty map W toMap() { all values are null }`() { - // Given - val accessibility = Accessibility() - - // When - val result = accessibility.toMap() - - // Then - assertThat(result).isEmpty() - } - - @Test - fun `M return complete map W toMap() { all values are provided }`( - @FloatForgery(min = 0.5f, max = 3.0f) textSize: Float, - @BoolForgery isScreenReaderEnabled: Boolean, - @BoolForgery isColorInversionEnabled: Boolean, - @BoolForgery isClosedCaptioningEnabled: Boolean, - @BoolForgery isReducedAnimationsEnabled: Boolean, - @BoolForgery isScreenPinningEnabled: Boolean - ) { - // Given - val accessibility = Accessibility( - textSize = textSize.toString(), - isScreenReaderEnabled = isScreenReaderEnabled, - isColorInversionEnabled = isColorInversionEnabled, - isClosedCaptioningEnabled = isClosedCaptioningEnabled, - isReducedAnimationsEnabled = isReducedAnimationsEnabled, - isScreenPinningEnabled = isScreenPinningEnabled - ) - - // When - val result = accessibility.toMap() - - // Then - assertThat(result).containsExactlyInAnyOrderEntriesOf( - mapOf( - Accessibility.TEXT_SIZE_KEY to textSize.toString(), - Accessibility.SCREEN_READER_ENABLED_KEY to isScreenReaderEnabled, - Accessibility.COLOR_INVERSION_ENABLED_KEY to isColorInversionEnabled, - Accessibility.CLOSED_CAPTIONING_ENABLED_KEY to isClosedCaptioningEnabled, - Accessibility.REDUCED_ANIMATIONS_ENABLED_KEY to isReducedAnimationsEnabled, - Accessibility.SCREEN_PINNING_ENABLED_KEY to isScreenPinningEnabled - ) - ) - } - - @Test - fun `M have correct constant values W key constants`() { - // Then - assertThat(Accessibility.TEXT_SIZE_KEY).isEqualTo("text_size") - assertThat(Accessibility.SCREEN_READER_ENABLED_KEY).isEqualTo("screen_reader_enabled") - assertThat(Accessibility.COLOR_INVERSION_ENABLED_KEY).isEqualTo("invert_colors_enabled") - assertThat(Accessibility.CLOSED_CAPTIONING_ENABLED_KEY).isEqualTo("closed_captioning_enabled") - assertThat(Accessibility.REDUCED_ANIMATIONS_ENABLED_KEY).isEqualTo("reduced_animations_enabled") - assertThat(Accessibility.SCREEN_PINNING_ENABLED_KEY).isEqualTo("single_app_mode_enabled") - } - - @Test - fun `M exclude null values from map W toMap() { mixed null and non-null values }`() { - // Given - val accessibility = Accessibility( - textSize = "1.5f", - isScreenReaderEnabled = null, - isColorInversionEnabled = true, - isClosedCaptioningEnabled = false, - isReducedAnimationsEnabled = null, - isScreenPinningEnabled = true - ) - - // When - val result = accessibility.toMap() - - // Then - assertThat(result).containsExactlyInAnyOrderEntriesOf( - mapOf( - Accessibility.TEXT_SIZE_KEY to "1.5f", - Accessibility.COLOR_INVERSION_ENABLED_KEY to true, - Accessibility.CLOSED_CAPTIONING_ENABLED_KEY to false, - Accessibility.SCREEN_PINNING_ENABLED_KEY to true - ) - ) - assertThat(result).doesNotContainKeys( - Accessibility.SCREEN_READER_ENABLED_KEY, - Accessibility.REDUCED_ANIMATIONS_ENABLED_KEY - ) - } -} diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/accessibility/DatadogAccessibilityReaderTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilityReaderTest.kt similarity index 80% rename from features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/accessibility/DatadogAccessibilityReaderTest.kt rename to features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilityReaderTest.kt index ae0d3eb495..1c492ceb24 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/accessibility/DatadogAccessibilityReaderTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilityReaderTest.kt @@ -16,16 +16,11 @@ import android.os.Build import android.os.Handler import android.provider.Settings import android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED +import android.view.View import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener import com.datadog.android.api.InternalLogger -import com.datadog.android.rum.internal.domain.accessibility.Accessibility.Companion.CLOSED_CAPTIONING_ENABLED_KEY -import com.datadog.android.rum.internal.domain.accessibility.Accessibility.Companion.COLOR_INVERSION_ENABLED_KEY -import com.datadog.android.rum.internal.domain.accessibility.Accessibility.Companion.REDUCED_ANIMATIONS_ENABLED_KEY -import com.datadog.android.rum.internal.domain.accessibility.Accessibility.Companion.SCREEN_PINNING_ENABLED_KEY -import com.datadog.android.rum.internal.domain.accessibility.Accessibility.Companion.SCREEN_READER_ENABLED_KEY -import com.datadog.android.rum.internal.domain.accessibility.Accessibility.Companion.TEXT_SIZE_KEY -import com.datadog.android.rum.internal.domain.accessibility.DatadogAccessibilityReader.Companion.CAPTIONING_ENABLED_KEY +import com.datadog.android.rum.internal.domain.accessibility.DefaultAccessibilityReader.Companion.CAPTIONING_ENABLED_KEY import com.datadog.android.rum.utils.forge.Configurator import com.datadog.tools.unit.annotations.TestTargetApi import com.datadog.tools.unit.extensions.ApiLevelExtension @@ -58,7 +53,7 @@ import java.util.concurrent.atomic.AtomicLong ) @MockitoSettings(strictness = Strictness.LENIENT) @ForgeConfiguration(Configurator::class) -internal class DatadogAccessibilityReaderTest { +internal class DefaultAccessibilityReaderTest { @Mock lateinit var mockContext: Context @@ -89,7 +84,7 @@ internal class DatadogAccessibilityReaderTest { @Mock lateinit var mockHandler: Handler - private lateinit var testedReader: DatadogAccessibilityReader + private lateinit var testedReader: DefaultAccessibilityReader @BeforeEach fun setup() { @@ -98,7 +93,12 @@ internal class DatadogAccessibilityReaderTest { setupDefaultMockBehavior() - testedReader = DatadogAccessibilityReader( + // Create reader with default setup - individual tests can recreate if they need specific mocks + testedReader = createReader() + } + + private fun createReader(): DefaultAccessibilityReader { + return DefaultAccessibilityReader( internalLogger = mockInternalLogger, applicationContext = mockContext, resources = mockResources, @@ -154,7 +154,7 @@ internal class DatadogAccessibilityReaderTest { @Test fun `M return state with initial accessibility values W getState { accessibility manager is null }`() { // Given - testedReader = DatadogAccessibilityReader( + testedReader = DefaultAccessibilityReader( internalLogger = mockInternalLogger, applicationContext = mockContext, resources = mockResources, @@ -168,12 +168,13 @@ internal class DatadogAccessibilityReaderTest { // When val result = testedReader.getState() - assertThat(result[SCREEN_READER_ENABLED_KEY]).isNull() - assertThat(result[CAPTIONING_ENABLED_KEY]).isNull() - assertThat(result[REDUCED_ANIMATIONS_ENABLED_KEY]).isNull() - assertThat(result[SCREEN_PINNING_ENABLED_KEY] as Boolean).isFalse() - assertThat(result[COLOR_INVERSION_ENABLED_KEY]).isNull() - assertThat(result[TEXT_SIZE_KEY]).isEqualTo("1.0") + assertThat(result.isScreenReaderEnabled).isNull() + assertThat(result.isClosedCaptioningEnabled).isNull() + assertThat(result.isReducedAnimationsEnabled).isNull() + assertThat(result.isScreenPinningEnabled).isFalse() + assertThat(result.isColorInversionEnabled).isNull() + assertThat(result.textSize).isEqualTo("1.0") + assertThat(result.isRtlEnabled).isFalse() } @Test @@ -183,7 +184,8 @@ internal class DatadogAccessibilityReaderTest { @BoolForgery isColorInversionEnabled: Boolean, @BoolForgery isClosedCaptioningEnabled: Boolean, @BoolForgery isReducedAnimationsEnabled: Boolean, - @BoolForgery isScreenPinningEnabled: Boolean + @BoolForgery isScreenPinningEnabled: Boolean, + @BoolForgery isRtlEnabled: Boolean ) { // Given mockConfiguration.fontScale = textSize @@ -203,7 +205,7 @@ internal class DatadogAccessibilityReaderTest { applicationContext = any(), key = eq(ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) ) - ) doReturn isColorInversionEnabled + ) doReturn if (isColorInversionEnabled) 1 else 0 whenever( mockSecureWrapper.getInt( @@ -211,7 +213,10 @@ internal class DatadogAccessibilityReaderTest { applicationContext = any(), key = eq(CAPTIONING_ENABLED_KEY) ) - ) doReturn isClosedCaptioningEnabled + ) doReturn if (isClosedCaptioningEnabled) 1 else 0 + + whenever(mockConfiguration.layoutDirection) doReturn + if (isRtlEnabled) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR val animationDurationValue = if (isReducedAnimationsEnabled) 0.0f else 1.0f whenever( @@ -222,16 +227,19 @@ internal class DatadogAccessibilityReaderTest { ) ) doReturn animationDurationValue + testedReader = createReader() + // When val result = testedReader.getState() // Then - assertThat(result[TEXT_SIZE_KEY]).isEqualTo(textSize.toString()) - assertThat(result[SCREEN_READER_ENABLED_KEY]).isEqualTo(isScreenReaderEnabled) - assertThat(result[SCREEN_PINNING_ENABLED_KEY]).isEqualTo(isScreenPinningEnabled) - assertThat(result[COLOR_INVERSION_ENABLED_KEY]).isEqualTo(isColorInversionEnabled) - assertThat(result[CLOSED_CAPTIONING_ENABLED_KEY]).isEqualTo(isClosedCaptioningEnabled) - assertThat(result[REDUCED_ANIMATIONS_ENABLED_KEY]).isEqualTo(isReducedAnimationsEnabled) + assertThat(result.textSize).isEqualTo(textSize.toString()) + assertThat(result.isScreenReaderEnabled).isEqualTo(isScreenReaderEnabled) + assertThat(result.isScreenPinningEnabled).isEqualTo(isScreenPinningEnabled) + assertThat(result.isColorInversionEnabled).isEqualTo(isColorInversionEnabled) + assertThat(result.isClosedCaptioningEnabled).isEqualTo(isClosedCaptioningEnabled) + assertThat(result.isReducedAnimationsEnabled).isEqualTo(isReducedAnimationsEnabled) + assertThat(result.isRtlEnabled).isEqualTo(isRtlEnabled) } // region Text Size Tests @@ -243,29 +251,37 @@ internal class DatadogAccessibilityReaderTest { // Given mockConfiguration.fontScale = fontScale + testedReader = createReader() + // When val result = testedReader.getState() // Then - assertThat(result[TEXT_SIZE_KEY]).isEqualTo(fontScale.toString()) + assertThat(result.textSize).isEqualTo(fontScale.toString()) } + // endregion // region Screen Reader Tests + @Test fun `M return screen reader state W getState { touch exploration enabled }`() { // Given whenever(mockAccessibilityManager.isTouchExplorationEnabled) doReturn true + testedReader = createReader() + // When val result = testedReader.getState() // Then - assertThat(result[SCREEN_READER_ENABLED_KEY] as Boolean).isTrue() + assertThat(result.isScreenReaderEnabled as Boolean).isTrue() } + // endregion // region Screen Pinning Tests + @TestTargetApi(Build.VERSION_CODES.LOLLIPOP) @Test fun `M return screen pinning state W getState { api below 23 }`( @@ -279,7 +295,7 @@ internal class DatadogAccessibilityReaderTest { val result = testedReader.getState() // Then - assertThat(result[SCREEN_PINNING_ENABLED_KEY]).isEqualTo(lockState) + assertThat(result.isScreenPinningEnabled).isEqualTo(lockState) } @TestTargetApi(Build.VERSION_CODES.M) @@ -288,9 +304,11 @@ internal class DatadogAccessibilityReaderTest { // Given whenever(mockActivityManager.lockTaskModeState) doReturn ActivityManager.LOCK_TASK_MODE_LOCKED + testedReader = createReader() + // When val result = testedReader.getState() - val isScreenPinningEnabled = result[SCREEN_PINNING_ENABLED_KEY] as Boolean + val isScreenPinningEnabled = result.isScreenPinningEnabled as Boolean // Then assertThat(isScreenPinningEnabled).isTrue() @@ -302,9 +320,11 @@ internal class DatadogAccessibilityReaderTest { // Given whenever(mockActivityManager.lockTaskModeState) doReturn ActivityManager.LOCK_TASK_MODE_PINNED + testedReader = createReader() + // When val result = testedReader.getState() - val isScreenPinningEnabled = result[SCREEN_PINNING_ENABLED_KEY] as Boolean + val isScreenPinningEnabled = result.isScreenPinningEnabled as Boolean // Then assertThat(isScreenPinningEnabled).isTrue() @@ -316,9 +336,11 @@ internal class DatadogAccessibilityReaderTest { // Given whenever(mockActivityManager.lockTaskModeState) doReturn ActivityManager.LOCK_TASK_MODE_NONE + testedReader = createReader() + // When val result = testedReader.getState() - val isScreenPinningEnabled = result[SCREEN_PINNING_ENABLED_KEY] as Boolean + val isScreenPinningEnabled = result.isScreenPinningEnabled as Boolean // Then assertThat(isScreenPinningEnabled).isFalse() @@ -327,7 +349,7 @@ internal class DatadogAccessibilityReaderTest { @Test fun `M return null for screen pinning W getState { activity manager is null }`() { // Given - testedReader = DatadogAccessibilityReader( + testedReader = DefaultAccessibilityReader( internalLogger = mockInternalLogger, applicationContext = mockContext, resources = mockResources, @@ -342,11 +364,13 @@ internal class DatadogAccessibilityReaderTest { val result = testedReader.getState() // Then - assertThat(result[SCREEN_PINNING_ENABLED_KEY]).isNull() + assertThat(result.isScreenPinningEnabled).isNull() } + // endregion // region Color Inversion Tests + @Test fun `M return color inversion state W getState { setting available }`( @BoolForgery isEnabled: Boolean @@ -358,13 +382,15 @@ internal class DatadogAccessibilityReaderTest { applicationContext = mockContext, key = ACCESSIBILITY_DISPLAY_INVERSION_ENABLED ) - ) doReturn isEnabled + ) doReturn if (isEnabled) 1 else 0 + + testedReader = createReader() // When val result = testedReader.getState() // Then - assertThat(result[COLOR_INVERSION_ENABLED_KEY]).isEqualTo(isEnabled) + assertThat(result.isColorInversionEnabled).isEqualTo(isEnabled) } @Test @@ -382,11 +408,13 @@ internal class DatadogAccessibilityReaderTest { val result = testedReader.getState() // Then - assertThat(result[COLOR_INVERSION_ENABLED_KEY]).isNull() + assertThat(result.isColorInversionEnabled).isNull() } + // endregion // region Closed Captioning Tests + @Test fun `M return closed captioning state W getState { setting available }`( @BoolForgery isEnabled: Boolean @@ -398,13 +426,15 @@ internal class DatadogAccessibilityReaderTest { applicationContext = mockContext, key = CAPTIONING_ENABLED_KEY ) - ) doReturn isEnabled + ) doReturn if (isEnabled) 1 else 0 + + testedReader = createReader() // When val result = testedReader.getState() // Then - assertThat(result[CLOSED_CAPTIONING_ENABLED_KEY]).isEqualTo(isEnabled) + assertThat(result.isClosedCaptioningEnabled).isEqualTo(isEnabled) } @Test @@ -422,11 +452,12 @@ internal class DatadogAccessibilityReaderTest { val result = testedReader.getState() // Then - assertThat(result[CLOSED_CAPTIONING_ENABLED_KEY]).isNull() + assertThat(result.isClosedCaptioningEnabled).isNull() } // endregion // region Reduced Animations Tests + @Test fun `M return true for reduced animations W getState { enabled }`() { // Given @@ -438,9 +469,10 @@ internal class DatadogAccessibilityReaderTest { ) ) doReturn 0.0f + testedReader = createReader() + // When - val result = testedReader.getState() - val isReducedAnimations = result[REDUCED_ANIMATIONS_ENABLED_KEY] as Boolean + val isReducedAnimations = testedReader.getState().isReducedAnimationsEnabled as Boolean // Then assertThat(isReducedAnimations).isTrue() @@ -457,9 +489,10 @@ internal class DatadogAccessibilityReaderTest { ) ) doReturn 1.0f + testedReader = createReader() + // When - val result = testedReader.getState() - val isReducedAnimations = result[REDUCED_ANIMATIONS_ENABLED_KEY] as Boolean + val isReducedAnimations = testedReader.getState().isReducedAnimationsEnabled as Boolean // Then assertThat(isReducedAnimations).isFalse() @@ -480,10 +513,46 @@ internal class DatadogAccessibilityReaderTest { val result = testedReader.getState() // Then - assertThat(result[REDUCED_ANIMATIONS_ENABLED_KEY]).isEqualTo(null) + assertThat(result.isReducedAnimationsEnabled).isNull() } // endregion + // region RTLEnabled Tests + + @Test + fun `M return true for isRtlEnabled W getState { enabled }`() { + // Given + whenever( + mockConfiguration.layoutDirection + ) doReturn View.LAYOUT_DIRECTION_RTL + + testedReader = createReader() + + // When + val isRtlEnabled = testedReader.getState().isRtlEnabled as Boolean + + // Then + assertThat(isRtlEnabled).isTrue() + } + + @Test + fun `M return false for isRtlEnabled W getState { not enabled }`() { + // Given + whenever( + mockConfiguration.layoutDirection + ) doReturn View.LAYOUT_DIRECTION_LTR + + testedReader = createReader() + + // When + val isRtlEnabled = testedReader.getState().isRtlEnabled as Boolean + + // Then + assertThat(isRtlEnabled).isFalse() + } + + // endregion + // region ComponentCallbacks Tests @Test fun `M update text size W onConfigurationChanged { font scale changes }`( @@ -495,7 +564,7 @@ internal class DatadogAccessibilityReaderTest { // Establish initial state val initialResult = testedReader.getState() - assertThat(initialResult[TEXT_SIZE_KEY]).isEqualTo(originalFontScale.toString()) + assertThat(initialResult.textSize).isEqualTo(originalFontScale.toString()) // Change configuration val newConfiguration = Configuration().apply { fontScale = newFontScale } @@ -506,7 +575,7 @@ internal class DatadogAccessibilityReaderTest { // Then val result = testedReader.getState() - assertThat(result[TEXT_SIZE_KEY]).isEqualTo(newFontScale.toString()) + assertThat(result.textSize).isEqualTo(newFontScale.toString()) } @Test @@ -515,6 +584,8 @@ internal class DatadogAccessibilityReaderTest { ) { // Given mockConfiguration.fontScale = fontScale + + testedReader = createReader() val initialResult = testedReader.getState() val sameConfiguration = Configuration().apply { this.fontScale = fontScale } @@ -524,7 +595,7 @@ internal class DatadogAccessibilityReaderTest { // Then val result = testedReader.getState() - assertThat(result[TEXT_SIZE_KEY]).isEqualTo(fontScale.toString()) + assertThat(result.textSize).isEqualTo(fontScale.toString()) assertThat(result).isEqualTo(initialResult) } @@ -558,7 +629,7 @@ internal class DatadogAccessibilityReaderTest { @Test fun `M handle null accessibility manager W cleanup { accessibility manager is null }`() { // Given - testedReader = DatadogAccessibilityReader( + testedReader = DefaultAccessibilityReader( internalLogger = mockInternalLogger, applicationContext = mockContext, resources = mockResources, @@ -587,7 +658,7 @@ internal class DatadogAccessibilityReaderTest { // First call to establish baseline val firstResult = testedReader.getState() - assertThat(firstResult[SCREEN_PINNING_ENABLED_KEY] as Boolean).isFalse() + assertThat(firstResult.isScreenPinningEnabled as Boolean).isFalse() // Change the underlying state whenever(mockActivityManager.lockTaskModeState) doReturn ActivityManager.LOCK_TASK_MODE_LOCKED @@ -596,12 +667,13 @@ internal class DatadogAccessibilityReaderTest { val secondResult = testedReader.getState() // Then - State should NOT be updated since polling doesn't happen within threshold - assertThat(secondResult[SCREEN_PINNING_ENABLED_KEY] as Boolean).isFalse() + assertThat(secondResult.isScreenPinningEnabled as Boolean).isFalse() } // endregion // region Listener Tests + @Test fun `M update state W displayInversionListener onChange { display inversion changes }`() { // Given @@ -611,7 +683,7 @@ internal class DatadogAccessibilityReaderTest { applicationContext = any(), key = eq(ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) ) - ) doReturn true + ) doReturn 1 testedReader.getState() @@ -623,7 +695,7 @@ internal class DatadogAccessibilityReaderTest { // Then val result = testedReader.getState() - assertThat(result[COLOR_INVERSION_ENABLED_KEY] as Boolean).isTrue() + assertThat(result.isColorInversionEnabled as Boolean).isTrue() } @Test @@ -635,7 +707,7 @@ internal class DatadogAccessibilityReaderTest { applicationContext = any(), key = eq(CAPTIONING_ENABLED_KEY) ) - ) doReturn true + ) doReturn 1 testedReader.getState() @@ -647,7 +719,7 @@ internal class DatadogAccessibilityReaderTest { // Then val result = testedReader.getState() - assertThat(result[CLOSED_CAPTIONING_ENABLED_KEY] as Boolean).isTrue() + assertThat(result.isClosedCaptioningEnabled as Boolean).isTrue() } @Test @@ -671,7 +743,7 @@ internal class DatadogAccessibilityReaderTest { // Then val result = testedReader.getState() - assertThat(result[REDUCED_ANIMATIONS_ENABLED_KEY] as Boolean).isTrue() + assertThat(result.isReducedAnimationsEnabled as Boolean).isTrue() } @Test @@ -689,11 +761,13 @@ internal class DatadogAccessibilityReaderTest { // Then val result = testedReader.getState() - assertThat(result[SCREEN_READER_ENABLED_KEY] as Boolean).isTrue() + assertThat(result.isScreenReaderEnabled as Boolean).isTrue() } + // endregion // region Polling Threshold Tests + @Test fun `M update lastPollTime W getState { after threshold exceeded }`() { // Given @@ -720,21 +794,6 @@ internal class DatadogAccessibilityReaderTest { assertThat(newPollTime).isGreaterThan(oldTime) } - @Test - fun `M handle double cleanup W cleanup { called multiple times }`() { - // Given - Initialize first - testedReader.getState() - - // When - Call cleanup multiple times - testedReader.cleanup() - testedReader.cleanup() // Second call - - // Then - Should cleanup only once - verify(mockAccessibilityManager, times(1)).removeTouchExplorationStateChangeListener(any()) - verify(mockContentResolver, times(3)).unregisterContentObserver(any()) // 3 observers × 2 calls - verify(mockContext, times(1)).unregisterComponentCallbacks(testedReader) - } - @Test fun `M return consistent state W getState { after listener updates }`() { // Given @@ -744,7 +803,7 @@ internal class DatadogAccessibilityReaderTest { applicationContext = any(), key = any() ) - ) doReturn false + ) doReturn 0 val initialResult = testedReader.getState() @@ -754,7 +813,7 @@ internal class DatadogAccessibilityReaderTest { applicationContext = any(), key = eq(ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) ) - ) doReturn true + ) doReturn 1 // When val listenerField = testedReader.javaClass.getDeclaredField("displayInversionListener") @@ -764,8 +823,9 @@ internal class DatadogAccessibilityReaderTest { // Then val updatedResult = testedReader.getState() - assertThat(updatedResult[COLOR_INVERSION_ENABLED_KEY] as Boolean).isTrue() - assertThat(updatedResult[COLOR_INVERSION_ENABLED_KEY]).isNotEqualTo(initialResult[COLOR_INVERSION_ENABLED_KEY]) + assertThat(updatedResult.isColorInversionEnabled as Boolean).isTrue() + assertThat(updatedResult.isColorInversionEnabled).isNotEqualTo(initialResult.isColorInversionEnabled) } + // endregion } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilitySnapshotManagerTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilitySnapshotManagerTest.kt index a5418ab489..722ce7f5db 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilitySnapshotManagerTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/accessibility/DefaultAccessibilitySnapshotManagerTest.kt @@ -6,12 +6,7 @@ package com.datadog.android.rum.internal.domain.accessibility -import com.datadog.android.rum.internal.domain.accessibility.Accessibility.Companion.CLOSED_CAPTIONING_ENABLED_KEY -import com.datadog.android.rum.internal.domain.accessibility.Accessibility.Companion.COLOR_INVERSION_ENABLED_KEY -import com.datadog.android.rum.internal.domain.accessibility.Accessibility.Companion.REDUCED_ANIMATIONS_ENABLED_KEY -import com.datadog.android.rum.internal.domain.accessibility.Accessibility.Companion.SCREEN_PINNING_ENABLED_KEY -import com.datadog.android.rum.internal.domain.accessibility.Accessibility.Companion.SCREEN_READER_ENABLED_KEY -import com.datadog.android.rum.internal.domain.accessibility.Accessibility.Companion.TEXT_SIZE_KEY +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.utils.forge.Configurator import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.BoolForgery @@ -39,7 +34,7 @@ import org.mockito.quality.Strictness internal class DefaultAccessibilitySnapshotManagerTest { @Mock - lateinit var mockAccessibilityReader: AccessibilityReader + lateinit var mockAccessibilityReader: InfoProvider private lateinit var testedManager: DefaultAccessibilitySnapshotManager @@ -51,13 +46,13 @@ internal class DefaultAccessibilitySnapshotManagerTest { @Test fun `M return empty accessibility W latestSnapshot() { no accessibility data }`() { // Given - whenever(mockAccessibilityReader.getState()) doReturn emptyMap() + whenever(mockAccessibilityReader.getState()) doReturn AccessibilityInfo() // When val result = testedManager.latestSnapshot() // Then - assertThat(result).isEqualTo(Accessibility()) + assertThat(result).isEqualTo(AccessibilityInfo()) } @Test @@ -67,16 +62,18 @@ internal class DefaultAccessibilitySnapshotManagerTest { @BoolForgery colorInversion: Boolean, @BoolForgery closedCaptioning: Boolean, @BoolForgery reducedAnimations: Boolean, - @BoolForgery screenPinning: Boolean + @BoolForgery screenPinning: Boolean, + @BoolForgery rtlEnabled: Boolean ) { // Given - val accessibilityState = mapOf( - TEXT_SIZE_KEY to textSize.toString(), - SCREEN_READER_ENABLED_KEY to screenReader, - COLOR_INVERSION_ENABLED_KEY to colorInversion, - CLOSED_CAPTIONING_ENABLED_KEY to closedCaptioning, - REDUCED_ANIMATIONS_ENABLED_KEY to reducedAnimations, - SCREEN_PINNING_ENABLED_KEY to screenPinning + val accessibilityState = AccessibilityInfo( + textSize = textSize.toString(), + isScreenReaderEnabled = screenReader, + isColorInversionEnabled = colorInversion, + isClosedCaptioningEnabled = closedCaptioning, + isReducedAnimationsEnabled = reducedAnimations, + isScreenPinningEnabled = screenPinning, + isRtlEnabled = rtlEnabled ) whenever(mockAccessibilityReader.getState()) doReturn accessibilityState @@ -85,13 +82,14 @@ internal class DefaultAccessibilitySnapshotManagerTest { // Then assertThat(result).isEqualTo( - Accessibility( + AccessibilityInfo( textSize = textSize.toString(), isScreenReaderEnabled = screenReader, isColorInversionEnabled = colorInversion, isClosedCaptioningEnabled = closedCaptioning, isReducedAnimationsEnabled = reducedAnimations, - isScreenPinningEnabled = screenPinning + isScreenPinningEnabled = screenPinning, + isRtlEnabled = rtlEnabled ) ) } @@ -102,9 +100,9 @@ internal class DefaultAccessibilitySnapshotManagerTest { @BoolForgery screenReader: Boolean ) { // Given - val accessibilityState = mapOf( - TEXT_SIZE_KEY to textSize.toString(), - SCREEN_READER_ENABLED_KEY to screenReader + val accessibilityState = AccessibilityInfo( + textSize = textSize.toString(), + isScreenReaderEnabled = screenReader ) whenever(mockAccessibilityReader.getState()) doReturn accessibilityState @@ -115,7 +113,7 @@ internal class DefaultAccessibilitySnapshotManagerTest { val result = testedManager.latestSnapshot() // Then - assertThat(result).isEqualTo(Accessibility()) + assertThat(result).isEqualTo(AccessibilityInfo()) } @Test @@ -126,18 +124,18 @@ internal class DefaultAccessibilitySnapshotManagerTest { @BoolForgery colorInversion: Boolean ) { // Given - val initialState = mapOf( - TEXT_SIZE_KEY to initialTextSize, - SCREEN_READER_ENABLED_KEY to screenReader, - COLOR_INVERSION_ENABLED_KEY to colorInversion + val initialState = AccessibilityInfo( + textSize = initialTextSize.toString(), + isScreenReaderEnabled = screenReader, + isColorInversionEnabled = colorInversion ) val newTextSize = rerollFloat(initialTextSize, forge) - val changedState = mapOf( - TEXT_SIZE_KEY to newTextSize.toString(), // Changed - SCREEN_READER_ENABLED_KEY to screenReader, // Same - COLOR_INVERSION_ENABLED_KEY to colorInversion // Same + val changedState = AccessibilityInfo( + textSize = newTextSize.toString(), // Changed + isScreenReaderEnabled = screenReader, // Same + isColorInversionEnabled = colorInversion // Same ) whenever(mockAccessibilityReader.getState()) @@ -150,7 +148,7 @@ internal class DefaultAccessibilitySnapshotManagerTest { // Then - Only changed value should be returned assertThat(result).isEqualTo( - Accessibility(textSize = newTextSize.toString()) + AccessibilityInfo(textSize = newTextSize.toString()) ) } @@ -161,13 +159,13 @@ internal class DefaultAccessibilitySnapshotManagerTest { @BoolForgery colorInversion: Boolean ) { // Given - val initialState = mapOf( - TEXT_SIZE_KEY to textSize.toString() + val initialState = AccessibilityInfo( + textSize = textSize.toString() ) - val expandedState = mapOf( - TEXT_SIZE_KEY to textSize.toString(), - SCREEN_READER_ENABLED_KEY to screenReader, - COLOR_INVERSION_ENABLED_KEY to colorInversion + val expandedState = AccessibilityInfo( + textSize = textSize.toString(), + isScreenReaderEnabled = screenReader, + isColorInversionEnabled = colorInversion ) whenever(mockAccessibilityReader.getState()) @@ -180,7 +178,7 @@ internal class DefaultAccessibilitySnapshotManagerTest { // Then - Only new values should be returned assertThat(result).isEqualTo( - Accessibility( + AccessibilityInfo( isScreenReaderEnabled = screenReader, isColorInversionEnabled = colorInversion ) @@ -193,14 +191,14 @@ internal class DefaultAccessibilitySnapshotManagerTest { @BoolForgery screenReader: Boolean ) { // Given - val initialState = mapOf( - TEXT_SIZE_KEY to textSize.toString(), - SCREEN_READER_ENABLED_KEY to screenReader, - COLOR_INVERSION_ENABLED_KEY to true + val initialState = AccessibilityInfo( + textSize = textSize.toString(), + isScreenReaderEnabled = screenReader, + isColorInversionEnabled = true ) - val stateWithNull = mapOf( - TEXT_SIZE_KEY to textSize.toString(), - SCREEN_READER_ENABLED_KEY to screenReader + val stateWithNull = AccessibilityInfo( + textSize = textSize.toString(), + isScreenReaderEnabled = screenReader ) whenever(mockAccessibilityReader.getState()) @@ -212,52 +210,24 @@ internal class DefaultAccessibilitySnapshotManagerTest { val result = testedManager.latestSnapshot() // Second call // Then - No changes should be reported (null values are filtered) - assertThat(result).isEqualTo(Accessibility()) + assertThat(result).isEqualTo(AccessibilityInfo()) } @Test - fun `M report change W latestSnapshot() { value changes from null to non-null }`( - @FloatForgery textSize: Float, - @BoolForgery colorInversion: Boolean - ) { - // Given - val initialState = mapOf( - TEXT_SIZE_KEY to textSize.toString() - ) - val stateWithValue = mapOf( - TEXT_SIZE_KEY to textSize.toString(), - COLOR_INVERSION_ENABLED_KEY to colorInversion // Changed from null to value - ) - - whenever(mockAccessibilityReader.getState()) - .doReturn(initialState) - .doReturn(stateWithValue) - - // When - testedManager.latestSnapshot() // First call - val result = testedManager.latestSnapshot() // Second call - - // Then - New non-null value should be reported - assertThat(result).isEqualTo( - Accessibility(isColorInversionEnabled = colorInversion) - ) - } - - @Test - fun `M handle missing keys gracefully W latestSnapshot() { key disappears from state }`( + fun `M not report null keys W latestSnapshot() { key disappears from state }`( @FloatForgery textSize: Float, @BoolForgery screenReader: Boolean, @BoolForgery colorInversion: Boolean ) { // Given - val completeState = mapOf( - TEXT_SIZE_KEY to textSize.toString(), - SCREEN_READER_ENABLED_KEY to screenReader, - COLOR_INVERSION_ENABLED_KEY to colorInversion + val completeState = AccessibilityInfo( + textSize = textSize.toString(), + isScreenReaderEnabled = screenReader, + isColorInversionEnabled = colorInversion ) - val incompleteState = mapOf( - TEXT_SIZE_KEY to textSize.toString(), - SCREEN_READER_ENABLED_KEY to screenReader + val incompleteState = AccessibilityInfo( + textSize = textSize.toString(), + isScreenReaderEnabled = screenReader // COLOR_INVERSION_ENABLED_KEY missing ) @@ -270,7 +240,7 @@ internal class DefaultAccessibilitySnapshotManagerTest { val result = testedManager.latestSnapshot() // Second call // Then - No changes should be reported for missing keys (they become null) - assertThat(result).isEqualTo(Accessibility()) + assertThat(result).isEqualTo(AccessibilityInfo()) } @Test @@ -282,10 +252,10 @@ internal class DefaultAccessibilitySnapshotManagerTest { val textSize2 = rerollFloat(textSize1, forge) val textSize3 = rerollFloat(textSize2, forge) - val state1 = mapOf(TEXT_SIZE_KEY to textSize1.toString()) - val state2 = mapOf(TEXT_SIZE_KEY to textSize2.toString()) - val state3 = mapOf(TEXT_SIZE_KEY to textSize2.toString()) // Same as state2 - val state4 = mapOf(TEXT_SIZE_KEY to textSize3.toString()) + val state1 = AccessibilityInfo(textSize = textSize1.toString()) + val state2 = AccessibilityInfo(textSize = textSize2.toString()) + val state3 = AccessibilityInfo(textSize = textSize2.toString()) // Same as state2 + val state4 = AccessibilityInfo(textSize = textSize3.toString()) whenever(mockAccessibilityReader.getState()) .doReturn(state1) @@ -301,7 +271,7 @@ internal class DefaultAccessibilitySnapshotManagerTest { assertThat(result2.textSize).isEqualTo(textSize2.toString()) val result3 = testedManager.latestSnapshot() - assertThat(result3).isEqualTo(Accessibility()) // No change + assertThat(result3).isEqualTo(AccessibilityInfo()) // No change val result4 = testedManager.latestSnapshot() assertThat(result4.textSize).isEqualTo(textSize3.toString()) diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/battery/DefaultBatteryInfoProviderTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/battery/DefaultBatteryInfoProviderTest.kt new file mode 100644 index 0000000000..e4162af21e --- /dev/null +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/battery/DefaultBatteryInfoProviderTest.kt @@ -0,0 +1,224 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.domain.battery + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.ContentResolver +import android.content.Context +import android.content.IntentFilter +import android.os.BatteryManager +import android.os.BatteryManager.BATTERY_PROPERTY_CAPACITY +import android.os.Build +import android.os.PowerManager +import com.datadog.android.rum.utils.forge.Configurator +import com.datadog.tools.unit.annotations.TestTargetApi +import com.datadog.tools.unit.extensions.ApiLevelExtension +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(ApiLevelExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DefaultBatteryInfoProviderTest { + private lateinit var testedProvider: DefaultBatteryInfoProvider + + @Mock + lateinit var mockApplicationContext: Context + + @Mock + lateinit var mockContentResolver: ContentResolver + + @Mock + lateinit var mockPowerManager: PowerManager + + @Mock + lateinit var mockSystemClockWrapper: SystemClockWrapper + + @Mock + lateinit var mockBatteryManager: BatteryManager + + private val testSuiteStartTime = System.currentTimeMillis() + + private val shortPollingInterval = 200 + + @BeforeEach + fun setup() { + whenever(mockApplicationContext.contentResolver) doReturn mockContentResolver + whenever(mockSystemClockWrapper.elapsedRealTime()) doReturn testSuiteStartTime + whenever(mockPowerManager.isPowerSaveMode) doReturn false + whenever(mockBatteryManager.getIntProperty(BATTERY_PROPERTY_CAPACITY)) doReturn 50 + initializeBatteryManager() + } + + // region getBatteryState + + @Test + fun `M return complete battery info W getBatteryState() { all services available }`( + @BoolForgery fakeLowPowerMode: Boolean, + @IntForgery(0, 100) fakeBatteryLevel: Int + ) { + // Given + whenever(mockPowerManager.isPowerSaveMode) doReturn fakeLowPowerMode + whenever(mockBatteryManager.getIntProperty(BATTERY_PROPERTY_CAPACITY)) doReturn fakeBatteryLevel + initializeBatteryManager() + + // When + val batteryInfo = testedProvider.getState() + val expectedBatteryPct = fakeBatteryLevel / 100f + + // Then + assertThat(batteryInfo.lowPowerMode).isEqualTo(fakeLowPowerMode) + assertThat(batteryInfo.batteryLevel).isEqualTo(expectedBatteryPct) + } + + @Test + fun `M return battery info with nulls W getBatteryState() { services unavailable }`() { + // Given + initializeBatteryManager(null, null) + + // When + val batteryInfo = testedProvider.getState() + + // Then + assertThat(batteryInfo).isNotNull + assertThat(batteryInfo.lowPowerMode).isNull() + assertThat(batteryInfo.batteryLevel).isNull() + } + + // endregion + + // region BroadcastReceiver Tests + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + @Test + fun `M register broadcast receiver W constructor { initialization }`() { + // Then + val receiverCaptor = argumentCaptor() + val filterCaptor = argumentCaptor() + verify(mockApplicationContext).registerReceiver(receiverCaptor.capture(), filterCaptor.capture()) + } + + // endregion + + // region Cleanup Tests + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + @Test + fun `M unregister receiver W cleanup() { after initialization }`() { + // When + testedProvider.cleanup() + + // Then + val receiverCaptor = argumentCaptor() + verify(mockApplicationContext).registerReceiver(receiverCaptor.capture(), any()) + verify(mockApplicationContext).unregisterReceiver(receiverCaptor.firstValue) + } + + @Test + fun `M retain initial state W cleanup() then getState() { no re-initialization }`() { + // When - cleanup (no re-initialization happens) + testedProvider.cleanup() + val batteryInfo = testedProvider.getState() + + // Then - should retain the initial state from constructor + assertThat(batteryInfo.lowPowerMode).isEqualTo(false) + assertThat(batteryInfo.batteryLevel).isEqualTo(0.5f) + } + + // endregion + + // region Polling Tests + + @Test + fun `M update battery level W getState() { after polling interval }`() { + // When + whenever(mockBatteryManager.getIntProperty(BATTERY_PROPERTY_CAPACITY)) doReturn 75 + + // nothing changes because we are within polling interval + assertThat(testedProvider.getState().batteryLevel).isEqualTo(0.5f) + whenever(mockSystemClockWrapper.elapsedRealTime()) doReturn testSuiteStartTime + shortPollingInterval / 2 + assertThat(testedProvider.getState().batteryLevel).isEqualTo(0.5f) + + // Then + // after polling interval level should change + whenever(mockSystemClockWrapper.elapsedRealTime()) doReturn testSuiteStartTime + shortPollingInterval + val batteryInfo = testedProvider.getState() + assertThat(batteryInfo.batteryLevel).isEqualTo(0.75f) + } + + // endregion + + // region Error Handling Tests + + @Test + @TestTargetApi(Build.VERSION_CODES.Q) + fun `M return null battery level W getBatteryLevel() { Integer MIN_VALUE }`() { + // Given + whenever(mockPowerManager.isPowerSaveMode) doReturn false + whenever(mockBatteryManager.getIntProperty(BATTERY_PROPERTY_CAPACITY)) doReturn Integer.MIN_VALUE + initializeBatteryManager() + + // When + val batteryInfo = testedProvider.getState() + + // Then + assertThat(batteryInfo.batteryLevel).isNull() + assertThat(batteryInfo.lowPowerMode).isEqualTo(false) + } + + @Test + @TestTargetApi(Build.VERSION_CODES.P) + fun `M return null battery level W getBatteryLevel() { zero - on old api }`() { + // Given + whenever(mockPowerManager.isPowerSaveMode) doReturn false + whenever(mockBatteryManager.getIntProperty(BATTERY_PROPERTY_CAPACITY)) doReturn 0 + initializeBatteryManager() + + // When + val batteryInfo = testedProvider.getState() + + // Then + assertThat(batteryInfo.batteryLevel).isNull() + assertThat(batteryInfo.lowPowerMode).isEqualTo(false) + } + + // endregion + + private fun initializeBatteryManager( + powerManager: PowerManager? = mockPowerManager, + batteryManager: BatteryManager? = mockBatteryManager + ) { + testedProvider = DefaultBatteryInfoProvider( + applicationContext = mockApplicationContext, + batteryLevelPollInterval = shortPollingInterval, + powerManager = powerManager, + batteryManager = batteryManager, + systemClockWrapper = mockSystemClockWrapper + ) + } +} diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/display/DefaultDisplayInfoProviderTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/display/DefaultDisplayInfoProviderTest.kt new file mode 100644 index 0000000000..6ff7a74943 --- /dev/null +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/display/DefaultDisplayInfoProviderTest.kt @@ -0,0 +1,158 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.domain.display + +import android.content.ContentResolver +import android.content.Context +import android.os.Handler +import android.provider.Settings.System.SCREEN_BRIGHTNESS +import com.datadog.android.api.InternalLogger +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@ExtendWith(MockitoExtension::class) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class DefaultDisplayInfoProviderTest { + private lateinit var testedProvider: DefaultDisplayInfoProvider + + @Mock + lateinit var mockApplicationContext: Context + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockHandler: Handler + + @Mock + lateinit var mockSettingsWrapper: SystemSettingsWrapper + + @Mock + lateinit var mockContentResolver: ContentResolver + + @BeforeEach + fun setup() { + whenever(mockApplicationContext.contentResolver) doReturn mockContentResolver + } + + @Test + fun `M return normalized brightness W getBrightnessLevel() { specific brightness values }`() { + val testCases = mapOf( + 0 to 0.0f, + 1 to 0.0f, // 1/255 = 0.004 rounds to 0.0 + 127 to 0.5f, // 127/255 = 0.498 rounds to 0.5 + 128 to 0.5f, // 128/255 = 0.502 rounds to 0.5 + 254 to 1.0f, // 254/255 = 0.996 rounds to 1.0 + 255 to 1.0f + ) + + testCases.forEach { (rawBrightness, expected) -> + // Given - create fresh provider for each test case to avoid initialization caching + whenever(mockSettingsWrapper.getInt(SCREEN_BRIGHTNESS)) doReturn rawBrightness + + createDisplayInfoProvider() + + // When + val result = testedProvider.getState().screenBrightness + + // Then + assertThat(result) + .`as`("Brightness $rawBrightness should normalize to $expected") + .isEqualTo(expected) + } + } + + @Test + fun `M verify rounding behavior W getBrightnessLevel() { test decimal precision }`() { + // Given + val rawBrightness = 191 // 191/255 = 0.749... should round to 0.7 + whenever(mockSettingsWrapper.getInt(SCREEN_BRIGHTNESS)) doReturn rawBrightness + + createDisplayInfoProvider() + + // When + val result = testedProvider.getState().screenBrightness + + // Then + assertThat(result).isEqualTo(0.7f) + } + + // region ContentObserver Tests - Integration Style + + @Test + fun `M retain initial state W cleanup() then getState() { no re-initialization }`() { + // Given - create provider + whenever(mockSettingsWrapper.getInt(SCREEN_BRIGHTNESS)) doReturn 128 + + createDisplayInfoProvider() + + // When - cleanup (no re-initialization happens) + testedProvider.cleanup() + val displayInfo = testedProvider.getState() + + // Then - should retain the initial state from constructor + assertThat(displayInfo.screenBrightness).isEqualTo(0.5f) + } + + // endregion + + // region Cleanup Tests + + @Test + fun `M unregister observer W cleanup() { after initialization }`() { + // Given - create provider (initialization happens in constructor) + whenever(mockSettingsWrapper.getInt(SCREEN_BRIGHTNESS)) doReturn 128 + + createDisplayInfoProvider() + + // When + testedProvider.cleanup() + + // Then + verify(mockContentResolver).unregisterContentObserver(any()) + } + + // endregion + + // region Error Handling Tests + + @Test + fun `M handle system settings error W getState() { initialization error }`() { + // Given + whenever(mockSettingsWrapper.getInt(SCREEN_BRIGHTNESS)) doReturn Integer.MIN_VALUE + + createDisplayInfoProvider() + + // When + val displayInfo = testedProvider.getState() + + // Then - should handle gracefully with null brightness + assertThat(displayInfo.screenBrightness).isNull() + } + + // endregion + + private fun createDisplayInfoProvider() { + testedProvider = DefaultDisplayInfoProvider( + applicationContext = mockApplicationContext, + internalLogger = mockInternalLogger, + systemSettingsWrapper = mockSettingsWrapper, + contentResolver = mockContentResolver, + handler = mockHandler + ) + } +} diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt index 8afb683b1c..67bc286232 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt @@ -18,8 +18,11 @@ import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum.RumSessionType +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.accessibility.AccessibilitySnapshotManager +import com.datadog.android.rum.internal.domain.battery.BatteryInfo +import com.datadog.android.rum.internal.domain.display.DisplayInfo import com.datadog.android.rum.internal.domain.state.ViewUIPerformanceReport import com.datadog.android.rum.internal.metric.SessionMetricDispatcher import com.datadog.android.rum.internal.metric.slowframes.SlowFramesListener @@ -86,6 +89,12 @@ internal class RumApplicationScopeTest { @Mock lateinit var mockMemoryVitalMonitor: VitalMonitor + @Mock + lateinit var mockBatteryInfoProvider: InfoProvider + + @Mock + lateinit var mockDisplayInfoProvider: InfoProvider + @Mock lateinit var mockFrameRateVitalMonitor: VitalMonitor @@ -159,7 +168,9 @@ internal class RumApplicationScopeTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt index 50cfb6be03..a958735f06 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt @@ -15,8 +15,11 @@ import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum.RumSessionType +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.accessibility.AccessibilitySnapshotManager +import com.datadog.android.rum.internal.domain.battery.BatteryInfo +import com.datadog.android.rum.internal.domain.display.DisplayInfo import com.datadog.android.rum.internal.metric.SessionMetricDispatcher import com.datadog.android.rum.internal.metric.slowframes.SlowFramesListener import com.datadog.android.rum.internal.vitals.VitalMonitor @@ -87,6 +90,12 @@ internal class RumSessionScopeTest { @Mock lateinit var mockAccessibilitySnapshotManager: AccessibilitySnapshotManager + @Mock + lateinit var mockBatteryInfoProvider: InfoProvider + + @Mock + lateinit var mockDisplayInfoProvider: InfoProvider + @Mock lateinit var mockSessionListener: RumSessionListener @@ -1325,7 +1334,9 @@ internal class RumSessionScopeTest { sessionInactivityNanos = TEST_INACTIVITY_NS, sessionMaxDurationNanos = TEST_MAX_DURATION_NS, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) if (withMockChildScope) { diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeTest.kt index b638c5b0b3..bfce08972a 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeTest.kt @@ -23,9 +23,12 @@ import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumSessionType import com.datadog.android.rum.internal.anr.ANRDetectorRunnable import com.datadog.android.rum.internal.anr.ANRException +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.domain.accessibility.AccessibilitySnapshotManager +import com.datadog.android.rum.internal.domain.battery.BatteryInfo +import com.datadog.android.rum.internal.domain.display.DisplayInfo import com.datadog.android.rum.internal.domain.state.ViewUIPerformanceReport import com.datadog.android.rum.internal.metric.SessionMetricDispatcher import com.datadog.android.rum.internal.metric.slowframes.SlowFramesListener @@ -128,6 +131,12 @@ internal class RumViewManagerScopeTest { @Mock lateinit var mockAccessibilitySnapshotManager: AccessibilitySnapshotManager + @Mock + lateinit var mockBatteryInfoProvider: InfoProvider + + @Mock + lateinit var mockDisplayInfoProvider: InfoProvider + @Mock lateinit var mockLastInteractionIdentifier: LastInteractionIdentifier @@ -173,7 +182,9 @@ internal class RumViewManagerScopeTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) } @@ -541,7 +552,9 @@ internal class RumViewManagerScopeTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) testedScope.applicationDisplayed = true val fakeEvent = forge.validBackgroundEvent() @@ -575,7 +588,9 @@ internal class RumViewManagerScopeTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) testedScope.childrenScopes.add(mockChildScope) whenever(mockChildScope.isActive()) doReturn true @@ -612,7 +627,9 @@ internal class RumViewManagerScopeTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) testedScope.applicationDisplayed = true val fakeEvent = forge.validBackgroundEvent() @@ -682,7 +699,9 @@ internal class RumViewManagerScopeTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) testedScope.childrenScopes.add(mockChildScope) whenever(mockChildScope.isActive()) doReturn true @@ -720,7 +739,9 @@ internal class RumViewManagerScopeTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) testedScope.stopped = true val fakeEvent = forge.applicationStartedEvent() diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt index 5e613979a8..2b3419f344 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt @@ -34,9 +34,12 @@ import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.internal.anr.ANRException import com.datadog.android.rum.internal.collections.toEvictingQueue +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.domain.accessibility.AccessibilitySnapshotManager +import com.datadog.android.rum.internal.domain.battery.BatteryInfo +import com.datadog.android.rum.internal.domain.display.DisplayInfo import com.datadog.android.rum.internal.domain.state.SlowFrameRecord import com.datadog.android.rum.internal.domain.state.ViewUIPerformanceReport import com.datadog.android.rum.internal.metric.NoValueReason @@ -155,6 +158,12 @@ internal class RumViewScopeTest { @Mock lateinit var mockAccessibilitySnapshotManager: AccessibilitySnapshotManager + @Mock + lateinit var mockBatteryInfoProvider: InfoProvider + + @Mock + lateinit var mockDisplayInfoProvider: InfoProvider + @Mock lateinit var mockMemoryVitalMonitor: VitalMonitor @@ -312,6 +321,9 @@ internal class RumViewScopeTest { val fakeNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(fakeOffset) val maxLimit = max(Long.MAX_VALUE - fakeTimestamp, Long.MAX_VALUE) val minLimit = min(-fakeTimestamp, maxLimit) + val fakeBrightness = forge.aFloat(0f, 255f) + val fakeBatteryLevel = forge.aFloat(0f, 100f) + val fakeLowPowerMode = forge.aBool() fakeSampleRate = forge.aFloat(min = 0.0f, max = 100.0f) fakeDatadogContext = fakeDatadogContext.copy( @@ -357,6 +369,16 @@ internal class RumViewScopeTest { } whenever(mockWriter.write(eq(mockEventBatchWriter), any(), eq(EventType.DEFAULT))) doReturn true fakeReplayStats = ViewEvent.ReplayStats(recordsCount = fakeReplayRecordsCount) + + // Mock battery and brightness providers + whenever(mockBatteryInfoProvider.getState()) doReturn BatteryInfo( + batteryLevel = fakeBatteryLevel, + lowPowerMode = fakeLowPowerMode + ) + whenever(mockDisplayInfoProvider.getState()) doReturn DisplayInfo( + screenBrightness = fakeBrightness + ) + testedScope = newRumViewScope(trackFrustrations = true) mockSessionReplayContext(testedScope) } @@ -7889,7 +7911,7 @@ internal class RumViewScopeTest { // GIVEN val frameTimeSeconds = forge.aDouble(min = 0.001, max = 0.05) // 1ms to 50ms val expectedRefreshRate = 1.0 / frameTimeSeconds - var expectedRefreshRateMin = expectedRefreshRate + val expectedRefreshRateMin = expectedRefreshRate // WHEN testedScope.handleEvent( @@ -9672,7 +9694,9 @@ internal class RumViewScopeTest { slowFramesListener = slowFramesMetricListener, viewEndedMetricDispatcher = viewEndedMetricDispatcher, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) data class RumRawEventData(val event: RumRawEvent, val viewKey: RumScopeKey) diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt index 262cf3f4a5..083306473b 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt @@ -31,8 +31,11 @@ import com.datadog.android.rum.RumSessionType import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.internal.RumFeature import com.datadog.android.rum.internal.debug.RumDebugListener +import com.datadog.android.rum.internal.domain.InfoProvider import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.accessibility.AccessibilitySnapshotManager +import com.datadog.android.rum.internal.domain.battery.BatteryInfo +import com.datadog.android.rum.internal.domain.display.DisplayInfo import com.datadog.android.rum.internal.domain.event.ResourceTiming import com.datadog.android.rum.internal.domain.scope.RumActionScope import com.datadog.android.rum.internal.domain.scope.RumApplicationScope @@ -124,6 +127,12 @@ internal class DatadogRumMonitorTest { @Mock lateinit var mockAccessibilitySnapshotManager: AccessibilitySnapshotManager + @Mock + lateinit var mockBatteryInfoProvider: InfoProvider + + @Mock + lateinit var mockDisplayInfoProvider: InfoProvider + @Mock lateinit var mockResolver: FirstPartyHostHeaderTypeResolver @@ -223,7 +232,9 @@ internal class DatadogRumMonitorTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) testedMonitor.rootScope = mockScope } @@ -250,7 +261,9 @@ internal class DatadogRumMonitorTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) val rootScope = testedMonitor.rootScope @@ -314,7 +327,9 @@ internal class DatadogRumMonitorTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) val completableFuture = CompletableFuture() testedMonitor.start() @@ -356,7 +371,9 @@ internal class DatadogRumMonitorTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) val completableFuture = CompletableFuture() @@ -1705,7 +1722,9 @@ internal class DatadogRumMonitorTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) // When @@ -1755,7 +1774,9 @@ internal class DatadogRumMonitorTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) // When @@ -1792,6 +1813,8 @@ internal class DatadogRumMonitorTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider, rumSessionTypeOverride = null ) whenever(mockExecutorService.isShutdown).thenReturn(true) @@ -1990,7 +2013,9 @@ internal class DatadogRumMonitorTest { lastInteractionIdentifier = mockLastInteractionIdentifier, slowFramesListener = mockSlowFramesListener, rumSessionTypeOverride = fakeRumSessionType, - accessibilitySnapshotManager = mockAccessibilitySnapshotManager + accessibilitySnapshotManager = mockAccessibilitySnapshotManager, + batteryInfoProvider = mockBatteryInfoProvider, + displayInfoProvider = mockDisplayInfoProvider ) testedMonitor.startView(key, name, attributes) // When