diff --git a/.editorconfig b/.editorconfig index fa30597745..d13785f5ce 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,10 +3,11 @@ # Copyright 2016-Present Datadog, Inc. [*.{kt,kts}] -ktlint_code_style = ktlint_official -indent_size = unset +ktlint_code_style = android_studio ij_kotlin_allow_trailing_comma_on_call_site = false ij_kotlin_allow_trailing_comma = false +ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^ +max_line_length = 120 # Code generated by KotlinPoet [buildSrc/src/test/kotlin/com/example/model/*.kt] diff --git a/CHANGELOG.md b/CHANGELOG.md index 14dc536987..a8b1ca4ac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +# 2.7.0 / 2024-03-21 + +* [FEATURE] Session Replay: Add a request builder for resources. See [#1827](https://github.com/DataDog/dd-sdk-android/pull/1827) +* [FEATURE] Session Replay: Add Resources feature. See [#1840](https://github.com/DataDog/dd-sdk-android/pull/1840) +* [FEATURE] Session Replay: Implement resource capture during traversal. See [#1854](https://github.com/DataDog/dd-sdk-android/pull/1854) +* [FEATURE] Add `source_type` when sent from cross platform logs. See [#1895](https://github.com/DataDog/dd-sdk-android/pull/1895) +* [FEATURE] Session Replay: Enable Resource Endpoint by default. See [#1858](https://github.com/DataDog/dd-sdk-android/pull/1858) +* [FEATURE] Logs: Add support for global attributes on logs. See [#1900](https://github.com/DataDog/dd-sdk-android/pull/1900) +* [FEATURE] RUM: Allow setting custom error fingerprint. See [#1911](https://github.com/DataDog/dd-sdk-android/pull/1911) +* [FEATURE] RUM: Report all threads for non-fatal ANRs. See [#1912](https://github.com/DataDog/dd-sdk-android/pull/1912) +* [FEATURE] RUM: Report fatal ANRs. See [#1909](https://github.com/DataDog/dd-sdk-android/pull/1909) +* [BUGFIX] Session Replay: Avoid crash when `applicationContext` is `null`. See [#1864](https://github.com/DataDog/dd-sdk-android/pull/1864) +* [BUGFIX] Session Replay: Fix image resizing issue. See [#1897](https://github.com/DataDog/dd-sdk-android/pull/1897) +* [BUGFIX] Fix typo in source type. See [#1904](https://github.com/DataDog/dd-sdk-android/pull/1904) +* [BUGFIX] RUM: Prevent `ConcurrentModificationException` when reading feature flags. See [#1925](https://github.com/DataDog/dd-sdk-android/pull/1925) +* [IMPROVEMENT] RUM: Disable non-fatal ANR reporting by default. See [#1914](https://github.com/DataDog/dd-sdk-android/pull/1914) +* [IMPROVEMENT] RUM: Introduce `error.category` attribute for exceptions, categorize ANRs separately. See [#1918](https://github.com/DataDog/dd-sdk-android/pull/1918) +* [MAINTENANCE] Next dev iteration. See [#1861](https://github.com/DataDog/dd-sdk-android/pull/1861) +* [MAINTENANCE] Merge `release/2.6.0` in `develop`. See [#1862](https://github.com/DataDog/dd-sdk-android/pull/1862) +* [MAINTENANCE] Merge `release/2.6.1` changes into `develop` branch. See [#1868](https://github.com/DataDog/dd-sdk-android/pull/1868) +* [MAINTENANCE] Update telemetry schema. See [#1874](https://github.com/DataDog/dd-sdk-android/pull/1874) +* [MAINTENANCE] Merge Hotfix 2.6.2. See [#1890](https://github.com/DataDog/dd-sdk-android/pull/1890) +* [MAINTENANCE] Add signed commits requirement to `CONTRIBUTING.md`. See [#1905](https://github.com/DataDog/dd-sdk-android/pull/1905) +* [MAINTENANCE] Session Replay: Cleanup SR code. See [#1910](https://github.com/DataDog/dd-sdk-android/pull/1910) +* [MAINTENANCE] Session Replay: Fix integration tests post Session Replay refactoring. See [#1916](https://github.com/DataDog/dd-sdk-android/pull/1916) +* [MAINTENANCE] Session Replay: Fix `SrImageButtonsMaskUserInputTest`. See [#1920](https://github.com/DataDog/dd-sdk-android/pull/1920) +* [MAINTENANCE] Adjust `ktlint` formatting rules. See [#1919](https://github.com/DataDog/dd-sdk-android/pull/1919) +* [MAINTENANCE] Fix formatting. See [#1921](https://github.com/DataDog/dd-sdk-android/pull/1921) + # 2.6.2 / 2024-02-23 * [BUGFIX] RUM: Fix crash in frame rate vital detection. See [#1872](https://github.com/DataDog/dd-sdk-android/pull/1872) diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/AndroidConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/AndroidConfig.kt index 3dbcf4f82f..2e904b4cc6 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/AndroidConfig.kt @@ -19,7 +19,7 @@ object AndroidConfig { const val MIN_SDK_FOR_WEAR = 23 const val BUILD_TOOLS_VERSION = "34.0.0" - val VERSION = Version(2, 7, 0, Version.Type.Snapshot) + val VERSION = Version(2, 8, 0, Version.Type.Snapshot) } // TODO RUM-628 Switch to Java 17 bytecode diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index 15c2e14ae6..75291e672d 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -96,6 +96,7 @@ interface com.datadog.android.api.feature.Feature const val RUM_FEATURE_NAME: String const val TRACING_FEATURE_NAME: String const val SESSION_REPLAY_FEATURE_NAME: String + const val NDK_CRASH_REPORTS_FEATURE_NAME: String interface com.datadog.android.api.feature.FeatureEventReceiver fun onReceive(Any) interface com.datadog.android.api.feature.FeatureScope @@ -145,7 +146,11 @@ interface com.datadog.android.core.InternalSdkCore : com.datadog.android.api.fea val rootStorageDir: java.io.File? val isDeveloperModeEnabled: Boolean val firstPartyHostResolver: com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver + val lastViewEvent: com.google.gson.JsonObject? + val lastFatalAnrSent: Long? fun writeLastViewEvent(ByteArray) + fun deleteLastViewEvent() + fun writeLastFatalAnrSent(Long) fun getPersistenceExecutorService(): java.util.concurrent.ExecutorService fun getAllFeatures(): List fun getDatadogContext(): com.datadog.android.api.context.DatadogContext? @@ -228,9 +233,9 @@ fun java.io.File.existsSafe(com.datadog.android.api.InternalLogger): Boolean fun java.io.File.readTextSafe(java.nio.charset.Charset = Charsets.UTF_8, com.datadog.android.api.InternalLogger): String? fun java.io.File.readLinesSafe(java.nio.charset.Charset = Charsets.UTF_8, com.datadog.android.api.InternalLogger): List? interface com.datadog.android.core.internal.system.BuildSdkVersionProvider - fun version(): Int -class com.datadog.android.core.internal.system.DefaultBuildSdkVersionProvider : BuildSdkVersionProvider - override fun version(): Int + val version: Int + companion object + val DEFAULT: BuildSdkVersionProvider class com.datadog.android.core.internal.thread.LoggingScheduledThreadPoolExecutor : java.util.concurrent.ScheduledThreadPoolExecutor constructor(Int, com.datadog.android.api.InternalLogger) override fun afterExecute(Runnable?, Throwable?) @@ -242,9 +247,12 @@ val NULL_MAP_VALUE: Object object com.datadog.android.core.internal.utils.JsonSerializer fun toJsonElement(Any?): com.google.gson.JsonElement fun Map.safeMapValuesToJson(com.datadog.android.api.InternalLogger): Map +const val HEX_RADIX: Int fun Int.toHexString(): String fun Long.toHexString(): String fun java.math.BigInteger.toHexString(): String +fun Thread.State.asString(): String +fun Array.loggableStackTrace(): String fun Throwable.loggableStackTrace(): String interface com.datadog.android.core.persistence.PersistenceStrategy interface Factory @@ -331,6 +339,7 @@ object com.datadog.android.log.LogAttributes const val USR_NAME: String const val VARIANT: String const val SOURCE_TYPE: String + const val ERROR_FINGERPRINT: String enum com.datadog.android.privacy.TrackingConsent - GRANTED - NOT_GRANTED diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api index 4ae514b637..d585a8e1fc 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -294,6 +294,7 @@ public final class com/datadog/android/api/context/UserInfo { public abstract interface class com/datadog/android/api/feature/Feature { public static final field Companion Lcom/datadog/android/api/feature/Feature$Companion; public static final field LOGS_FEATURE_NAME Ljava/lang/String; + public static final field NDK_CRASH_REPORTS_FEATURE_NAME Ljava/lang/String; public static final field RUM_FEATURE_NAME Ljava/lang/String; public static final field SESSION_REPLAY_FEATURE_NAME Ljava/lang/String; public static final field TRACING_FEATURE_NAME Ljava/lang/String; @@ -304,6 +305,7 @@ public abstract interface class com/datadog/android/api/feature/Feature { public final class com/datadog/android/api/feature/Feature$Companion { public static final field LOGS_FEATURE_NAME Ljava/lang/String; + public static final field NDK_CRASH_REPORTS_FEATURE_NAME Ljava/lang/String; public static final field RUM_FEATURE_NAME Ljava/lang/String; public static final field SESSION_REPLAY_FEATURE_NAME Ljava/lang/String; public static final field TRACING_FEATURE_NAME Ljava/lang/String; @@ -430,14 +432,18 @@ public final class com/datadog/android/api/storage/RawBatchEvent { } public abstract interface class com/datadog/android/core/InternalSdkCore : com/datadog/android/api/feature/FeatureSdkCore { + public abstract fun deleteLastViewEvent ()V public abstract fun getAllFeatures ()Ljava/util/List; public abstract fun getDatadogContext ()Lcom/datadog/android/api/context/DatadogContext; public abstract fun getFirstPartyHostResolver ()Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver; + public abstract fun getLastFatalAnrSent ()Ljava/lang/Long; + public abstract fun getLastViewEvent ()Lcom/google/gson/JsonObject; public abstract fun getNetworkInfo ()Lcom/datadog/android/api/context/NetworkInfo; public abstract fun getPersistenceExecutorService ()Ljava/util/concurrent/ExecutorService; public abstract fun getRootStorageDir ()Ljava/io/File; public abstract fun getTrackingConsent ()Lcom/datadog/android/privacy/TrackingConsent; public abstract fun isDeveloperModeEnabled ()Z + public abstract fun writeLastFatalAnrSent (J)V public abstract fun writeLastViewEvent ([B)V } @@ -624,12 +630,12 @@ public final class com/datadog/android/core/internal/persistence/file/FileExtKt } public abstract interface class com/datadog/android/core/internal/system/BuildSdkVersionProvider { - public abstract fun version ()I + public static final field Companion Lcom/datadog/android/core/internal/system/BuildSdkVersionProvider$Companion; + public abstract fun getVersion ()I } -public final class com/datadog/android/core/internal/system/DefaultBuildSdkVersionProvider : com/datadog/android/core/internal/system/BuildSdkVersionProvider { - public fun ()V - public fun version ()I +public final class com/datadog/android/core/internal/system/BuildSdkVersionProvider$Companion { + public final fun getDEFAULT ()Lcom/datadog/android/core/internal/system/BuildSdkVersionProvider; } public final class com/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor : java/util/concurrent/ScheduledThreadPoolExecutor { @@ -658,11 +664,17 @@ public final class com/datadog/android/core/internal/utils/MapUtilsKt { } public final class com/datadog/android/core/internal/utils/NumberExtKt { + public static final field HEX_RADIX I public static final fun toHexString (I)Ljava/lang/String; public static final fun toHexString (J)Ljava/lang/String; public static final fun toHexString (Ljava/math/BigInteger;)Ljava/lang/String; } +public final class com/datadog/android/core/internal/utils/ThreadExtKt { + public static final fun asString (Ljava/lang/Thread$State;)Ljava/lang/String; + public static final fun loggableStackTrace ([Ljava/lang/StackTraceElement;)Ljava/lang/String; +} + public final class com/datadog/android/core/internal/utils/ThrowableExtKt { public static final fun loggableStackTrace (Ljava/lang/Throwable;)Ljava/lang/String; } @@ -754,6 +766,7 @@ public final class com/datadog/android/log/LogAttributes { public static final field DD_TRACE_ID Ljava/lang/String; public static final field DURATION Ljava/lang/String; public static final field ENV Ljava/lang/String; + public static final field ERROR_FINGERPRINT Ljava/lang/String; public static final field ERROR_KIND Ljava/lang/String; public static final field ERROR_MESSAGE Ljava/lang/String; public static final field ERROR_SOURCE_TYPE Ljava/lang/String; diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/Feature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/Feature.kt index b85b02bc72..295731da66 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/Feature.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/Feature.kt @@ -53,5 +53,10 @@ interface Feature { * Session Replay feature name. */ const val SESSION_REPLAY_FEATURE_NAME: String = "session-replay" + + /** + * NDK Crash Reports feature name. + */ + const val NDK_CRASH_REPORTS_FEATURE_NAME: String = "ndk-crash-reporting" } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/DatadogCore.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/DatadogCore.kt index b082b457a6..e8c652022a 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/DatadogCore.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/DatadogCore.kt @@ -9,6 +9,7 @@ package com.datadog.android.core import android.app.Application import android.content.Context import android.content.pm.ApplicationInfo +import android.os.Build import android.util.Log import androidx.annotation.WorkerThread import com.datadog.android.Datadog @@ -22,7 +23,6 @@ import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureEventReceiver import com.datadog.android.api.feature.FeatureScope import com.datadog.android.api.feature.FeatureSdkCore -import com.datadog.android.api.storage.RawBatchEvent import com.datadog.android.core.configuration.BatchSize import com.datadog.android.core.configuration.Configuration import com.datadog.android.core.configuration.UploadFrequency @@ -32,14 +32,12 @@ import com.datadog.android.core.internal.SdkFeature import com.datadog.android.core.internal.lifecycle.ProcessLifecycleCallback import com.datadog.android.core.internal.lifecycle.ProcessLifecycleMonitor import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver -import com.datadog.android.core.internal.persistence.file.FileWriter -import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter -import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.system.BuildSdkVersionProvider import com.datadog.android.core.internal.utils.scheduleSafe import com.datadog.android.error.internal.CrashReportsFeature -import com.datadog.android.ndk.internal.DatadogNdkCrashHandler import com.datadog.android.ndk.internal.NdkCrashHandler import com.datadog.android.privacy.TrackingConsent +import com.google.gson.JsonObject import java.io.File import java.util.Locale import java.util.concurrent.ExecutorService @@ -52,6 +50,7 @@ import java.util.concurrent.TimeUnit * @param name the name of this instance * @param internalLoggerProvider Provider for [InternalLogger] instance. * @param persistenceExecutorServiceFactory Custom factory for persistence executor, used only in unit-tests + * @param buildSdkVersionProvider Build.VERSION.SDK_INT provider used for the test */ @Suppress("TooManyFunctions") internal class DatadogCore( @@ -60,7 +59,8 @@ internal class DatadogCore( override val name: String, internalLoggerProvider: (FeatureSdkCore) -> InternalLogger = { SdkInternalLogger(it) }, // only for unit tests - private val persistenceExecutorServiceFactory: ((InternalLogger) -> ExecutorService)? = null + private val persistenceExecutorServiceFactory: ((InternalLogger) -> ExecutorService)? = null, + private val buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT ) : InternalSdkCore { internal lateinit var coreFeature: CoreFeature @@ -81,13 +81,6 @@ internal class DatadogCore( internal val isActive: Boolean get() = coreFeature.initialized.get() - private val ndkLastViewEventFileWriter: FileWriter by lazy { - BatchFileReaderWriter.create( - internalLogger = internalLogger, - encryption = coreFeature.localDataEncryption - ) - } - private var processLifecycleMonitor: ProcessLifecycleMonitor? = null // region SdkCore @@ -183,6 +176,10 @@ internal class DatadogCore( features.values.forEach { it.clearAllData() } + @Suppress("ThreadSafety") // removal of the data is done in synchronous manner + coreFeature.deleteLastViewEvent() + @Suppress("ThreadSafety") // removal of the data is done in synchronous manner + coreFeature.deleteLastFatalAnrSent() } /** @inheritDoc */ @@ -245,23 +242,41 @@ internal class DatadogCore( override val rootStorageDir: File get() = coreFeature.storageDir + @get:WorkerThread + override val lastViewEvent: JsonObject? + get() = coreFeature.lastViewEvent + + @get:WorkerThread + override val lastFatalAnrSent: Long? + get() = coreFeature.lastFatalAnrSent + @WorkerThread override fun writeLastViewEvent(data: ByteArray) { - // directory structure may not exist: currently it is a file which is located in NDK reports - // folder, so if NDK reporting plugin is not initialized, this NDK reports dir won't exist - // as well (and no need to write). - val lastViewEventFile = DatadogNdkCrashHandler.getLastViewEventFile(coreFeature.storageDir) - if (lastViewEventFile.parentFile?.existsSafe(internalLogger) == true) { - ndkLastViewEventFileWriter.writeData(lastViewEventFile, RawBatchEvent(data), false) + // we need to write it only if we are going to read ApplicationExitInfo (available on + // API 30+) or if there is NDK crash tracking enabled + if (buildSdkVersionProvider.version >= Build.VERSION_CODES.R || + features.containsKey(Feature.NDK_CRASH_REPORTS_FEATURE_NAME) + ) { + coreFeature.writeLastViewEvent(data) } else { internalLogger.log( - InternalLogger.Level.WARN, + InternalLogger.Level.INFO, InternalLogger.Target.MAINTAINER, - { LAST_VIEW_EVENT_DIR_MISSING_MESSAGE.format(Locale.US, lastViewEventFile.parent) } + { NO_NEED_TO_WRITE_LAST_VIEW_EVENT } ) } } + @WorkerThread + override fun deleteLastViewEvent() { + coreFeature.deleteLastViewEvent() + } + + @WorkerThread + override fun writeLastFatalAnrSent(anrTimestamp: Long) { + coreFeature.writeLastFatalAnrSent(anrTimestamp) + } + override fun getPersistenceExecutorService(): ExecutorService { return coreFeature.persistenceExecutorService } @@ -487,8 +502,9 @@ internal class DatadogCore( internal const val EVENT_RECEIVER_ALREADY_EXISTS = "Feature \"%s\" already has event receiver registered, overwriting it." - const val LAST_VIEW_EVENT_DIR_MISSING_MESSAGE = "Directory structure %s for writing" + - " last view event doesn't exist." + internal const val NO_NEED_TO_WRITE_LAST_VIEW_EVENT = + "No need to write last RUM view event: NDK" + + " crash reports feature is not enabled and API is below 30." internal val CONFIGURATION_TELEMETRY_DELAY_MS = TimeUnit.SECONDS.toMillis(5) } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/InternalSdkCore.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/InternalSdkCore.kt index e8ea3a17f4..5f33cf73e7 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/InternalSdkCore.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/InternalSdkCore.kt @@ -15,6 +15,7 @@ import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.lint.InternalApi import com.datadog.android.privacy.TrackingConsent +import com.google.gson.JsonObject import java.io.File import java.util.concurrent.ExecutorService @@ -54,6 +55,20 @@ interface InternalSdkCore : FeatureSdkCore { */ val firstPartyHostResolver: FirstPartyHostHeaderTypeResolver + /** + * Reads last known RUM view event stored. + */ + @get:WorkerThread + @InternalApi + val lastViewEvent: JsonObject? + + /** + * Reads information about last fatal ANR sent. + */ + @get:WorkerThread + @InternalApi + val lastFatalAnrSent: Long? + /** * Writes current RUM view event to the dedicated file for the needs of NDK crash reporting. * @@ -63,6 +78,20 @@ interface InternalSdkCore : FeatureSdkCore { @WorkerThread fun writeLastViewEvent(data: ByteArray) + /** + * Deletes last RUM view event written. + */ + @InternalApi + @WorkerThread + fun deleteLastViewEvent() + + /** + * Writes timestamp of the last fatal ANR sent. + */ + @InternalApi + @WorkerThread + fun writeLastFatalAnrSent(anrTimestamp: Long) + /** * Get an executor service for persistence purposes. * @return the persistence executor to use for this SDK diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/NoOpInternalSdkCore.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/NoOpInternalSdkCore.kt index 32bdc37095..77447625fd 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/NoOpInternalSdkCore.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/NoOpInternalSdkCore.kt @@ -17,6 +17,7 @@ import com.datadog.android.api.feature.FeatureScope import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.privacy.TrackingConsent +import com.google.gson.JsonObject import java.io.File import java.util.concurrent.Callable import java.util.concurrent.ExecutorService @@ -57,6 +58,10 @@ internal object NoOpInternalSdkCore : InternalSdkCore { get() = false override val firstPartyHostResolver: FirstPartyHostHeaderTypeResolver get() = DefaultFirstPartyHostHeaderTypeResolver(emptyMap()) + override val lastViewEvent: JsonObject? + get() = null + override val lastFatalAnrSent: Long? + get() = null // endregion @@ -100,6 +105,10 @@ internal object NoOpInternalSdkCore : InternalSdkCore { override fun writeLastViewEvent(data: ByteArray) = Unit + override fun deleteLastViewEvent() = Unit + + override fun writeLastFatalAnrSent(anrTimestamp: Long) = Unit + override fun getPersistenceExecutorService(): ExecutorService = NoOpExecutorService() override fun getAllFeatures(): List = emptyList() diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt index a1216dcc27..2a012bf1ff 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt @@ -18,6 +18,7 @@ import com.datadog.android.BuildConfig import com.datadog.android.Datadog import com.datadog.android.DatadogSite import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.RawBatchEvent import com.datadog.android.core.allowThreadDiskReads import com.datadog.android.core.configuration.BatchProcessingLevel import com.datadog.android.core.configuration.BatchSize @@ -36,8 +37,13 @@ import com.datadog.android.core.internal.persistence.JsonObjectDeserializer import com.datadog.android.core.internal.persistence.file.FileMover import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.FileWriter import com.datadog.android.core.internal.persistence.file.advanced.ScheduledWriter import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter +import com.datadog.android.core.internal.persistence.file.deleteSafe +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.file.readTextSafe +import com.datadog.android.core.internal.persistence.file.writeTextSafe import com.datadog.android.core.internal.privacy.ConsentProvider import com.datadog.android.core.internal.privacy.NoOpConsentProvider import com.datadog.android.core.internal.privacy.TrackingConsentProvider @@ -72,6 +78,7 @@ import com.datadog.android.ndk.internal.NdkUserInfoDataWriter import com.datadog.android.ndk.internal.NoOpNdkCrashHandler import com.datadog.android.privacy.TrackingConsent import com.datadog.android.security.Encryption +import com.google.gson.JsonObject import com.lyft.kronos.AndroidClockFactory import com.lyft.kronos.KronosClock import okhttp3.CipherSuite @@ -145,6 +152,37 @@ internal class CoreFeature( internal val featuresContext: MutableMap> = ConcurrentHashMap() + // lazy here on purpose: we need to read it only once, even if it is used in different features + @get:WorkerThread + internal val lastViewEvent: JsonObject? by lazy { + @Suppress("ThreadSafety") // called in worker thread context + val viewEvent = readLastViewEvent() + if (viewEvent != null) { + @Suppress("ThreadSafety") // called in worker thread context + deleteLastViewEvent() + } + viewEvent + } + + @get:WorkerThread + private val lastViewEventFile: File by lazy { File(storageDir, LAST_RUM_VIEW_EVENT_FILE_NAME) } + private val lastViewEventFileWriter: FileWriter by lazy { + BatchFileReaderWriter.create( + internalLogger = internalLogger, + encryption = localDataEncryption + ) + } + + internal val lastFatalAnrSent: Long? + get() { + val file = File(storageDir, LAST_FATAL_ANR_SENT_FILE_NAME) + return if (file.existsSafe(internalLogger)) { + file.readTextSafe(Charsets.UTF_8, internalLogger)?.toLongOrNull() + } else { + null + } + } + fun initialize( appContext: Context, sdkInstanceId: String, @@ -256,19 +294,81 @@ internal class CoreFeature( // region Internal + @WorkerThread + internal fun writeLastViewEvent(data: ByteArray) { + lastViewEventFileWriter.writeData(lastViewEventFile, RawBatchEvent(data), false) + } + + @WorkerThread + internal fun deleteLastViewEvent() { + if (lastViewEventFile.existsSafe(internalLogger)) { + lastViewEventFile.deleteSafe(internalLogger) + } else { + @Suppress("DEPRECATION") + val legacyViewEventFile = DatadogNdkCrashHandler.getLastViewEventFile(storageDir) + if (legacyViewEventFile.existsSafe(internalLogger)) { + legacyViewEventFile.deleteSafe(internalLogger) + } + } + } + + @WorkerThread + internal fun writeLastFatalAnrSent(anrTimestamp: Long) { + // TODO RUMM-0000 this is temporary solution for storing just a timestamp, later we will + // migrate to a dedicated data store solution (same applies to the last RUM view event) + val file = File(storageDir, LAST_FATAL_ANR_SENT_FILE_NAME) + file.writeTextSafe(anrTimestamp.toString(), Charsets.UTF_8, internalLogger) + } + + @WorkerThread + internal fun deleteLastFatalAnrSent() { + val file = File(storageDir, LAST_FATAL_ANR_SENT_FILE_NAME) + if (file.existsSafe(internalLogger)) { + file.deleteSafe(internalLogger) + } + } + + @WorkerThread + private fun readLastViewEvent(): JsonObject? { + val lastViewEventFile = if (lastViewEventFile.existsSafe(internalLogger)) { + lastViewEventFile + } else { + @Suppress("DEPRECATION") + val legacyViewEventFile = DatadogNdkCrashHandler.getLastViewEventFile(storageDir) + if (legacyViewEventFile.existsSafe(internalLogger)) { + legacyViewEventFile + } else { + null + } + } + + if (lastViewEventFile == null) return null + + val reader = + BatchFileReaderWriter.create(internalLogger, localDataEncryption) + val content = reader.readData(lastViewEventFile) + return if (content.isEmpty()) { + null + } else { + @Suppress("UnsafeThirdPartyFunctionCall") // safe to call last, collection is not empty + String(content.last().data, Charsets.UTF_8).run { + JsonObjectDeserializer(internalLogger).deserialize(this) + } + } + } + private fun prepareNdkCrashData(nativeSourceType: String?) { if (isMainProcess) { ndkCrashHandler = DatadogNdkCrashHandler( storageDir, persistenceExecutorService, NdkCrashLogDeserializer(internalLogger), - rumEventDeserializer = JsonObjectDeserializer(internalLogger), NetworkInfoDeserializer(internalLogger), UserInfoDeserializer(internalLogger), internalLogger, - rumFileReader = BatchFileReaderWriter.create(internalLogger, localDataEncryption), envFileReader = FileReaderWriter.create(internalLogger, localDataEncryption), - nativeSourceType ?: "ndk" + lastRumViewEventProvider = { lastViewEvent }, + nativeCrashSourceType = nativeSourceType ?: "ndk" ) ndkCrashHandler.prepareData() } @@ -542,6 +642,9 @@ internal class CoreFeature( internal const val DEFAULT_SDK_VERSION = BuildConfig.SDK_VERSION_NAME internal const val DEFAULT_APP_VERSION = "?" + internal const val LAST_RUM_VIEW_EVENT_FILE_NAME = "last_view_event" + internal const val LAST_FATAL_ANR_SENT_FILE_NAME = "last_fatal_anr_sent" + internal val RESTRICTED_CIPHER_SUITES = arrayOf( // TLS 1.3 diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProvider.kt index d256ddc76b..5db0a4b095 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProvider.kt @@ -19,14 +19,13 @@ import com.datadog.android.api.context.NetworkInfo import com.datadog.android.core.internal.persistence.DataWriter import com.datadog.android.core.internal.receiver.ThreadSafeReceiver import com.datadog.android.core.internal.system.BuildSdkVersionProvider -import com.datadog.android.core.internal.system.DefaultBuildSdkVersionProvider import android.net.NetworkInfo as AndroidNetworkInfo @Suppress("DEPRECATION") @SuppressLint("InlinedApi") internal class BroadcastReceiverNetworkInfoProvider( private val dataWriter: DataWriter, - private val buildSdkVersionProvider: BuildSdkVersionProvider = DefaultBuildSdkVersionProvider() + private val buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT ) : ThreadSafeReceiver(), NetworkInfoProvider { @@ -104,7 +103,7 @@ internal class BroadcastReceiverNetworkInfoProvider( } val cellularTechnology = getCellularTechnology(subtype) - return if (buildSdkVersionProvider.version() >= Build.VERSION_CODES.P) { + return if (buildSdkVersionProvider.version >= Build.VERSION_CODES.P) { val telephonyMgr = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager val carrierName = telephonyMgr?.simCarrierIdName ?: UNKNOWN_CARRIER_NAME diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProvider.kt index 4feb892915..b3643362ec 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProvider.kt @@ -17,12 +17,11 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.context.NetworkInfo import com.datadog.android.core.internal.persistence.DataWriter import com.datadog.android.core.internal.system.BuildSdkVersionProvider -import com.datadog.android.core.internal.system.DefaultBuildSdkVersionProvider @TargetApi(Build.VERSION_CODES.N) internal class CallbackNetworkInfoProvider( private val dataWriter: DataWriter, - private val buildSdkVersionProvider: BuildSdkVersionProvider = DefaultBuildSdkVersionProvider(), + private val buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT, private val internalLogger: InternalLogger ) : ConnectivityManager.NetworkCallback(), @@ -164,7 +163,7 @@ internal class CallbackNetworkInfoProvider( @SuppressLint("NewApi") private fun resolveStrength(networkCapabilities: NetworkCapabilities): Long? { - return if (buildSdkVersionProvider.version() >= Build.VERSION_CODES.Q && + return if (buildSdkVersionProvider.version >= Build.VERSION_CODES.Q && networkCapabilities.signalStrength != NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED ) { networkCapabilities.signalStrength.toLong() diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt index 71769c0ef7..d3abeaa92a 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt @@ -182,3 +182,16 @@ fun File.readLinesSafe( null } } + +internal fun File.writeTextSafe( + text: String, + charset: Charset = Charsets.UTF_8, + internalLogger: InternalLogger +) { + if (existsSafe(internalLogger) && canWriteSafe(internalLogger)) { + safeCall(default = null, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + writeText(text, charset) + } + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/BuildSdkVersionProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/BuildSdkVersionProvider.kt index 22edfb319f..062d8a1de2 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/BuildSdkVersionProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/BuildSdkVersionProvider.kt @@ -7,6 +7,7 @@ package com.datadog.android.core.internal.system import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast /** * Wrapper around [Build.VERSION.SDK_INT] in order to simplify mocking in tests. @@ -18,5 +19,17 @@ interface BuildSdkVersionProvider { /** * Value of [Build.VERSION.SDK_INT]. */ - fun version(): Int + val version: Int + + companion object { + + /** + * Default implementation which calls Build.VERSION under the hood. + */ + val DEFAULT: BuildSdkVersionProvider = object : BuildSdkVersionProvider { + + @ChecksSdkIntAtLeast + override val version: Int = Build.VERSION.SDK_INT + } + } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/DefaultBuildSdkVersionProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/DefaultBuildSdkVersionProvider.kt deleted file mode 100644 index f1a491c210..0000000000 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/system/DefaultBuildSdkVersionProvider.kt +++ /dev/null @@ -1,20 +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.core.internal.system - -import android.os.Build - -/** - * Check [BuildSdkVersionProvider] docs. - * - * FOR INTERNAL USAGE ONLY. - */ -class DefaultBuildSdkVersionProvider : BuildSdkVersionProvider { - - /** @inheritdoc */ - override fun version(): Int = Build.VERSION.SDK_INT -} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/NumberExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/NumberExt.kt index 5661a1f242..6103d9b66e 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/NumberExt.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/NumberExt.kt @@ -9,7 +9,10 @@ package com.datadog.android.core.internal.utils import com.datadog.android.lint.InternalApi import java.math.BigInteger -internal const val HEX_RADIX = 16 +/** + * Radix used to convert numbers to hexadecimal strings. + */ +const val HEX_RADIX: Int = 16 /** * Converts [Int] into hexadecimal representation. diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/ThreadExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/ThreadExt.kt index 897224801e..68efab6b47 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/ThreadExt.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/utils/ThreadExt.kt @@ -6,11 +6,14 @@ package com.datadog.android.core.internal.utils +import com.datadog.android.lint.InternalApi + /** * Converts Thread state to string format. This is needed, because enum may be obfuscated, so we * cannot rely on the name property. */ -internal fun Thread.State.asString(): String { +@InternalApi +fun Thread.State.asString(): String { return when (this) { Thread.State.NEW -> "new" Thread.State.BLOCKED -> "blocked" @@ -24,4 +27,5 @@ internal fun Thread.State.asString(): String { /** * Converts stacktrace to string format. */ -internal fun Array.loggableStackTrace(): String = joinToString("\n") { "at $it" } +@InternalApi +fun Array.loggableStackTrace(): String = joinToString("\n") { "at $it" } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/log/LogAttributes.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/log/LogAttributes.kt index cc388331b8..2b7e3711f0 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/log/LogAttributes.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/log/LogAttributes.kt @@ -329,4 +329,9 @@ object LogAttributes { * or platform that the error originates from, such as Flutter or React Native (String). */ const val SOURCE_TYPE: String = "_dd.error.source_type" + + /** + * Specifies a custom error fingerprint for the supplied log. (String) + */ + const val ERROR_FINGERPRINT: String = "_dd.error.fingerprint" } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandler.kt index 612383b431..f0f7c362f9 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandler.kt @@ -14,11 +14,9 @@ import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.persistence.file.FileReader -import com.datadog.android.core.internal.persistence.file.batch.BatchFileReader import com.datadog.android.core.internal.persistence.file.existsSafe import com.datadog.android.core.internal.persistence.file.listFilesSafe import com.datadog.android.core.internal.persistence.file.readTextSafe -import com.datadog.android.core.internal.utils.join import com.datadog.android.core.internal.utils.submitSafe import com.datadog.android.log.LogAttributes import com.google.gson.JsonObject @@ -31,12 +29,11 @@ internal class DatadogNdkCrashHandler( storageDir: File, private val dataPersistenceExecutorService: ExecutorService, private val ndkCrashLogDeserializer: Deserializer, - private val rumEventDeserializer: Deserializer, private val networkInfoDeserializer: Deserializer, private val userInfoDeserializer: Deserializer, private val internalLogger: InternalLogger, - private val rumFileReader: BatchFileReader, private val envFileReader: FileReader, + private val lastRumViewEventProvider: () -> JsonObject?, internal val nativeCrashSourceType: String = "ndk" ) : NdkCrashHandler { @@ -80,6 +77,8 @@ internal class DatadogNdkCrashHandler( return } try { + lastRumViewEvent = lastRumViewEventProvider() + ndkCrashDataDirectory.listFilesSafe(internalLogger)?.forEach { file -> when (file.name) { // TODO RUMM-1944 Data from NDK should be also encrypted @@ -89,13 +88,6 @@ internal class DatadogNdkCrashHandler( ndkCrashLogDeserializer.deserialize(it) } - RUM_VIEW_EVENT_FILE_NAME -> - lastRumViewEvent = - readRumFileContent( - file, - rumFileReader - )?.let { rumEventDeserializer.deserialize(it) } - USER_INFO_FILE_NAME -> lastUserInfo = readFileContent( @@ -136,7 +128,7 @@ internal class DatadogNdkCrashHandler( InternalLogger.Level.ERROR, InternalLogger.Target.TELEMETRY, { - "Decoded file (${file.name}) content contains NULL character, file content={$it}," + + "Decoded file (${file.name}) content contains NULL character, file content={$it}," + " raw_bytes=${content.joinToString(",")}" } ) @@ -145,16 +137,6 @@ internal class DatadogNdkCrashHandler( } } - @WorkerThread - private fun readRumFileContent(file: File, fileReader: BatchFileReader): String? { - val content = fileReader.readData(file) - return if (content.isEmpty()) { - null - } else { - String(content.map { it.data }.join(ByteArray(0), internalLogger = internalLogger)) - } - } - @WorkerThread private fun checkAndHandleNdkCrashReport( sdkCore: FeatureSdkCore, @@ -363,7 +345,7 @@ internal class DatadogNdkCrashHandler( companion object { - internal const val RUM_VIEW_EVENT_FILE_NAME = "last_view_event" + private const val RUM_VIEW_EVENT_FILE_NAME = "last_view_event" internal const val CRASH_DATA_FILE_NAME = "crash_log" internal const val USER_INFO_FILE_NAME = "user_information" internal const val NETWORK_INFO_FILE_NAME = "network_information" @@ -395,6 +377,10 @@ internal class DatadogNdkCrashHandler( return File(storageDir, NDK_CRASH_REPORTS_PENDING_FOLDER_NAME) } + @Deprecated( + "We will still process this path to check file from the old SDK" + + " versions, but don't use it anymore for writing." + ) internal fun getLastViewEventFile(storageDir: File): File { return File(getNdkGrantedDir(storageDir), RUM_VIEW_EVENT_FILE_NAME) } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashHandler.kt index 9eac129817..6e1aac6c01 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashHandler.kt @@ -11,6 +11,7 @@ import com.datadog.tools.annotation.NoOpImplementation @NoOpImplementation internal interface NdkCrashHandler { + fun prepareData() fun handleNdkCrash(sdkCore: FeatureSdkCore, reportTarget: ReportTarget) diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt index 21f2d70831..76ace66b28 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt @@ -7,6 +7,7 @@ package com.datadog.android.core import android.app.Application +import android.os.Build import com.datadog.android.api.InternalLogger import com.datadog.android.api.context.NetworkInfo import com.datadog.android.api.context.TimeInfo @@ -21,22 +22,23 @@ import com.datadog.android.core.internal.lifecycle.ProcessLifecycleMonitor import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver import com.datadog.android.core.internal.net.info.NetworkInfoProvider import com.datadog.android.core.internal.privacy.ConsentProvider +import com.datadog.android.core.internal.system.BuildSdkVersionProvider import com.datadog.android.core.internal.time.NoOpTimeProvider import com.datadog.android.core.internal.time.TimeProvider import com.datadog.android.core.internal.user.MutableUserInfoProvider -import com.datadog.android.ndk.internal.DatadogNdkCrashHandler import com.datadog.android.ndk.internal.NdkCrashHandler import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.security.Encryption import com.datadog.android.utils.config.ApplicationContextTestConfiguration import com.datadog.android.utils.forge.Configurator import com.datadog.android.utils.verifyLog import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration +import com.google.gson.JsonObject import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.AdvancedForgery import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.LongForgery import fr.xgouchet.elmyr.annotation.MapForgery import fr.xgouchet.elmyr.annotation.StringForgery @@ -49,14 +51,12 @@ 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.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource 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.argThat import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn @@ -67,7 +67,6 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.mockito.quality.Strictness -import java.io.File import java.util.Locale import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit @@ -86,7 +85,7 @@ import java.util.concurrent.atomic.AtomicReference @ForgeConfiguration(Configurator::class) internal class DatadogCoreTest { - lateinit var testedCore: DatadogCore + private lateinit var testedCore: DatadogCore @Mock lateinit var mockInternalLogger: InternalLogger @@ -94,6 +93,9 @@ internal class DatadogCoreTest { @Mock lateinit var mockPersistenceExecutorService: ExecutorService + @Mock + lateinit var mockBuildSdkVersionProvider: BuildSdkVersionProvider + @Forgery lateinit var fakeConfiguration: Configuration @@ -119,7 +121,8 @@ internal class DatadogCoreTest { fakeInstanceId, fakeInstanceName, internalLoggerProvider = { mockInternalLogger }, - persistenceExecutorServiceFactory = { mockPersistenceExecutorService } + persistenceExecutorServiceFactory = { mockPersistenceExecutorService }, + buildSdkVersionProvider = mockBuildSdkVersionProvider ).apply { initialize(fakeConfiguration) } @@ -485,6 +488,36 @@ internal class DatadogCoreTest { assertThat(networkInfo).isSameAs(fakeNetworkInfo) } + @Test + fun `𝕄 provide last view event 𝕎 lastViewEvent()`( + @Forgery fakeLastViewEvent: JsonObject + ) { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.lastViewEvent) doReturn fakeLastViewEvent + + // When + val lastViewEvent = testedCore.lastViewEvent + + // Then + assertThat(lastViewEvent).isSameAs(fakeLastViewEvent) + } + + @Test + fun `𝕄 provide last fatal ANR sent 𝕎 lastFatalAnrSent()`( + @LongForgery(min = 0L) fakeLastFatalAnrSent: Long + ) { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.lastFatalAnrSent) doReturn fakeLastFatalAnrSent + + // When + val lastFatalAnrSent = testedCore.lastFatalAnrSent + + // Then + assertThat(lastFatalAnrSent).isEqualTo(fakeLastFatalAnrSent) + } + @Test fun `𝕄 return tracking consent 𝕎 trackingConsent()`( @Forgery fakeTrackingConsent: TrackingConsent @@ -509,58 +542,48 @@ internal class DatadogCoreTest { } @Test - fun `𝕄 persist the event into the NDK crash folder 𝕎 writeLastViewEvent(){ViewEvent+dir exists}`( - @TempDir tempStorageDir: File, + fun `𝕄 persist the event 𝕎 writeLastViewEvent(){ NDK feature registered }`( @StringForgery viewEvent: String ) { // Given val fakeViewEvent = viewEvent.toByteArray() - - val ndkReportsFolder = File( - tempStorageDir, - DatadogNdkCrashHandler.NDK_CRASH_REPORTS_FOLDER_NAME - ) - ndkReportsFolder.mkdir() + testedCore.features += Feature.NDK_CRASH_REPORTS_FEATURE_NAME to mock() val mockCoreFeature = mock() - whenever(mockCoreFeature.storageDir) doReturn tempStorageDir + testedCore.coreFeature = mockCoreFeature + + // When + testedCore.writeLastViewEvent(fakeViewEvent) - val mockEncryption = mock() - whenever(mockCoreFeature.localDataEncryption) doReturn mockEncryption - whenever(mockEncryption.encrypt(fakeViewEvent)) doReturn fakeViewEvent.reversedArray() - whenever(mockEncryption.encrypt(argThat { isEmpty() })) doAnswer { it.getArgument(0) } + // Then + verify(mockCoreFeature).writeLastViewEvent(fakeViewEvent) + } + @Test + fun `𝕄 persist the event 𝕎 writeLastViewEvent(){ R+ }`( + @StringForgery viewEvent: String, + @IntForgery(min = Build.VERSION_CODES.R) fakeSdkVersion: Int + ) { + // Given + val fakeViewEvent = viewEvent.toByteArray() + whenever(mockBuildSdkVersionProvider.version) doReturn fakeSdkVersion + val mockCoreFeature = mock() testedCore.coreFeature = mockCoreFeature // When testedCore.writeLastViewEvent(fakeViewEvent) // Then - val lastViewEventFile = File( - ndkReportsFolder, - DatadogNdkCrashHandler.RUM_VIEW_EVENT_FILE_NAME - ) - assertThat(lastViewEventFile).exists() - - val fileContent = lastViewEventFile.readBytes() - // file will have batch file format, so beginning will contain some metadata, - // we need to skip it for the comparison - val payload = fileContent.takeLast(fakeViewEvent.size).toByteArray() - assertThat(payload) - .isEqualTo(fakeViewEvent.reversedArray()) + verify(mockCoreFeature).writeLastViewEvent(fakeViewEvent) } @Test - fun `𝕄 log info when writing last view event 𝕎 writeLastViewEvent(){ ViewEvent+no crash dir }`( - @TempDir tempStorageDir: File, - @StringForgery viewEvent: String + fun `𝕄 log info when writing last view event 𝕎 writeLastViewEvent(){ below R and no NDK feature }`( + @StringForgery viewEvent: String, + @IntForgery(min = 1, max = Build.VERSION_CODES.R) fakeSdkVersion: Int ) { // Given - val ndkReportsFolder = File( - tempStorageDir, - DatadogNdkCrashHandler.NDK_CRASH_REPORTS_FOLDER_NAME - ) val mockCoreFeature = mock() - whenever(mockCoreFeature.storageDir) doReturn tempStorageDir + whenever(mockBuildSdkVersionProvider.version) doReturn fakeSdkVersion testedCore.coreFeature = mockCoreFeature reset(mockInternalLogger) @@ -568,17 +591,41 @@ internal class DatadogCoreTest { testedCore.writeLastViewEvent(viewEvent.toByteArray()) // Then - assertThat(ndkReportsFolder).doesNotExist() mockInternalLogger.verifyLog( - InternalLogger.Level.WARN, + InternalLogger.Level.INFO, InternalLogger.Target.MAINTAINER, - DatadogCore.LAST_VIEW_EVENT_DIR_MISSING_MESSAGE.format( - Locale.US, - ndkReportsFolder - ) + DatadogCore.NO_NEED_TO_WRITE_LAST_VIEW_EVENT ) } + @Test + fun `𝕄 delete last view event 𝕎 deleteLastViewEvent()`() { + // Given + val mockCoreFeature = mock() + testedCore.coreFeature = mockCoreFeature + + // When + testedCore.deleteLastViewEvent() + + // Then + verify(mockCoreFeature).deleteLastViewEvent() + } + + @Test + fun `𝕄 write last fatal ANR sent 𝕎 writeLastFatalAnrSent()`( + @LongForgery(min = 0L) fakeLastFatalAnrSent: Long + ) { + // Given + val mockCoreFeature = mock() + testedCore.coreFeature = mockCoreFeature + + // When + testedCore.writeLastFatalAnrSent(fakeLastFatalAnrSent) + + // Then + verify(mockCoreFeature).writeLastFatalAnrSent(fakeLastFatalAnrSent) + } + @Test fun `𝕄 clear data in all features 𝕎 clearAllData()`( forge: Forge @@ -591,6 +638,8 @@ internal class DatadogCoreTest { anAlphaNumericalString() to mock() } ) + val mockCoreFeature = mock() + testedCore.coreFeature = mockCoreFeature // When testedCore.clearAllData() @@ -599,6 +648,8 @@ internal class DatadogCoreTest { testedCore.features.forEach { verify(it.value).clearAllData() } + verify(mockCoreFeature).deleteLastFatalAnrSent() + verify(mockCoreFeature).deleteLastViewEvent() } @Test diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/ConfigurationBuilderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/ConfigurationBuilderTest.kt index 2686712500..a90fe04e4f 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/ConfigurationBuilderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/ConfigurationBuilderTest.kt @@ -121,7 +121,7 @@ internal class ConfigurationBuilderTest { fun `𝕄 build config with first party hosts 𝕎 setFirstPartyHosts() { ip addresses }`( @StringForgery( regex = "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}" + - "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])" + "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])" ) hosts: List ) { // When @@ -148,7 +148,7 @@ internal class ConfigurationBuilderTest { fun `𝕄 build config with first party hosts 𝕎 setFirstPartyHosts() { host names }`( @StringForgery( regex = "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\\.)+" + - "([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])" + "([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])" ) hosts: List ) { // When @@ -202,7 +202,7 @@ internal class ConfigurationBuilderTest { fun `𝕄 sanitize hosts 𝕎 setFirstPartyHosts()`( @StringForgery( regex = "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\\.)+" + - "([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])" + "([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])" ) hosts: List ) { // When @@ -224,7 +224,7 @@ internal class ConfigurationBuilderTest { fun `𝕄 build config with first party hosts and header types 𝕎 setFirstPartyHostsWithHeaderType() { host names }`( @StringForgery( regex = "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\\.)+" + - "([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])" + "([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])" ) hosts: List, forge: Forge ) { diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt index 986781ebe4..bb445c9e78 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt @@ -16,11 +16,13 @@ import android.os.Build import android.os.Process import com.datadog.android.Datadog import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.RawBatchEvent import com.datadog.android.core.configuration.Configuration import com.datadog.android.core.internal.net.info.BroadcastReceiverNetworkInfoProvider import com.datadog.android.core.internal.net.info.CallbackNetworkInfoProvider import com.datadog.android.core.internal.net.info.NoOpNetworkInfoProvider import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter import com.datadog.android.core.internal.privacy.ConsentProvider import com.datadog.android.core.internal.privacy.NoOpConsentProvider import com.datadog.android.core.internal.privacy.TrackingConsentProvider @@ -43,10 +45,12 @@ import com.datadog.tools.unit.assertj.containsInstanceOf import com.datadog.tools.unit.extensions.ApiLevelExtension import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration +import com.google.gson.JsonObject import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.AdvancedForgery import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery import fr.xgouchet.elmyr.annotation.MapForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.annotation.StringForgeryType @@ -886,7 +890,7 @@ internal class CoreFeatureTest { } @Test - fun `𝕄 initialise persitence strategy 𝕎 initialize`() { + fun `𝕄 initialise persistence strategy 𝕎 initialize`() { // Given val mockPersistenceStrategyFactory = mock() fakeConfig = fakeConfig.copy( @@ -910,6 +914,195 @@ internal class CoreFeatureTest { // endregion + @Test + fun `𝕄 return last fatal ANR sent 𝕎 lastFatalAnrSent`( + @TempDir tempDir: File, + @LongForgery(min = 0L) fakeLastFatalAnrSent: Long + ) { + // Given + testedFeature.storageDir = tempDir + File(tempDir, CoreFeature.LAST_FATAL_ANR_SENT_FILE_NAME) + .writeText(fakeLastFatalAnrSent.toString()) + + // When + val lastFatalAnrSent = testedFeature.lastFatalAnrSent + + // Then + assertThat(lastFatalAnrSent).isEqualTo(fakeLastFatalAnrSent) + } + + @Test + fun `𝕄 return null 𝕎 lastFatalAnrSent { no file }`( + @TempDir tempDir: File + ) { + // Given + testedFeature.storageDir = tempDir + + // When + val lastFatalAnrSent = testedFeature.lastFatalAnrSent + + // Then + assertThat(lastFatalAnrSent).isNull() + } + + @Test + fun `𝕄 return null 𝕎 lastFatalAnrSent { file contains not a number }`( + @TempDir tempDir: File, + @StringForgery fakeBrokenLastFatalAnrSent: String + ) { + // Given + testedFeature.storageDir = tempDir + File(tempDir, CoreFeature.LAST_FATAL_ANR_SENT_FILE_NAME) + .writeText(fakeBrokenLastFatalAnrSent) + + // When + val lastFatalAnrSent = testedFeature.lastFatalAnrSent + + // Then + assertThat(lastFatalAnrSent).isNull() + } + + @Test + fun `𝕄 delete last fatal ANR sent 𝕎 deleteLastFatalAnrSent`( + @TempDir tempDir: File, + @LongForgery fakeLastFatalAnrSent: Long + ) { + // Given + testedFeature.storageDir = tempDir + File(tempDir, CoreFeature.LAST_FATAL_ANR_SENT_FILE_NAME) + .writeText(fakeLastFatalAnrSent.toString()) + + // When + testedFeature.deleteLastFatalAnrSent() + + // Then + assertThat(File(tempDir, CoreFeature.LAST_FATAL_ANR_SENT_FILE_NAME)).doesNotExist() + } + + @Test + fun `𝕄 write last view event 𝕎 writeLastViewEvent`( + @TempDir tempDir: File, + @StringForgery viewEvent: String + ) { + // Given + val fakeViewEvent = viewEvent.toByteArray() + + testedFeature.storageDir = tempDir + + // When + testedFeature.writeLastViewEvent(fakeViewEvent) + + // Then + val lastViewEventFile = File( + tempDir, + CoreFeature.LAST_RUM_VIEW_EVENT_FILE_NAME + ) + assertThat(lastViewEventFile).exists() + + val fileContent = lastViewEventFile.readBytes() + // file will have batch file format, so beginning will contain some metadata, + // we need to skip it for the comparison + val payload = fileContent.takeLast(fakeViewEvent.size).toByteArray() + assertThat(payload).isEqualTo(fakeViewEvent) + } + + @Test + fun `𝕄 delete last view event 𝕎 deleteLastViewEvent`( + @TempDir tempDir: File, + @StringForgery fakeViewEvent: String + ) { + // Given + testedFeature.storageDir = tempDir + File(tempDir, CoreFeature.LAST_RUM_VIEW_EVENT_FILE_NAME) + .writeText(fakeViewEvent) + + // When + testedFeature.deleteLastViewEvent() + + // Then + assertThat(File(tempDir, CoreFeature.LAST_RUM_VIEW_EVENT_FILE_NAME)).doesNotExist() + } + + @Suppress("DEPRECATION") + @Test + fun `𝕄 delete last view event 𝕎 deleteLastViewEvent { legacy NDK location }`( + @TempDir tempDir: File, + @StringForgery fakeViewEvent: String + ) { + // Given + testedFeature.storageDir = tempDir + DatadogNdkCrashHandler.getLastViewEventFile(tempDir) + .apply { + parentFile?.mkdirs() + } + .writeText(fakeViewEvent) + + // When + testedFeature.deleteLastViewEvent() + + // Then + assertThat(DatadogNdkCrashHandler.getLastViewEventFile(tempDir)).doesNotExist() + } + + @Test + fun `𝕄 return null 𝕎 lastViewEvent { no last view event written }`( + @TempDir tempDir: File + ) { + // Given + testedFeature.storageDir = tempDir + + // When + val lastViewEvent = testedFeature.lastViewEvent + + // Then + assertThat(lastViewEvent).isNull() + } + + @Test + fun `𝕄 return last view event 𝕎 lastViewEvent`( + @TempDir tempDir: File, + @Forgery fakeViewEvent: JsonObject + ) { + // Given + testedFeature.storageDir = tempDir + testedFeature.writeLastViewEvent(fakeViewEvent.toString().toByteArray()) + + // When + val lastViewEvent = testedFeature.lastViewEvent + + // Then + assertThat(lastViewEvent.toString()).isEqualTo(fakeViewEvent.toString()) + // file must be deleted once view event is read + assertThat(File(tempDir, CoreFeature.LAST_RUM_VIEW_EVENT_FILE_NAME)).doesNotExist() + } + + @Test + fun `𝕄 return last view event 𝕎 lastViewEvent { check old NDK location }`( + @TempDir tempDir: File, + @Forgery fakeViewEvent: JsonObject + ) { + // Given + testedFeature.storageDir = tempDir + + @Suppress("DEPRECATION") + val legacyNdkViewEventFile = DatadogNdkCrashHandler.getLastViewEventFile(tempDir) + legacyNdkViewEventFile.parentFile?.mkdirs() + + BatchFileReaderWriter + .create(internalLogger = mock(), encryption = null) + .writeData( + legacyNdkViewEventFile, + RawBatchEvent(fakeViewEvent.toString().toByteArray()), + append = false + ) + + // When + val lastViewEvent = testedFeature.lastViewEvent + + // Then + assertThat(lastViewEvent.toString()).isEqualTo(fakeViewEvent.toString()) + } + // region shutdown @Test diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcherTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcherTest.kt index f5406c1e1e..fa709c71c7 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcherTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcherTest.kt @@ -269,8 +269,8 @@ internal class BatchMetricsDispatcherTest { eq(InternalLogger.Target.MAINTAINER), argThat { this.invoke() == - BatchMetricsDispatcher.WRONG_FILE_NAME_MESSAGE_FORMAT - .format(Locale.ENGLISH, fakeFile.name) + BatchMetricsDispatcher.WRONG_FILE_NAME_MESSAGE_FORMAT + .format(Locale.ENGLISH, fakeFile.name) }, eq(null), eq(false), @@ -510,8 +510,8 @@ internal class BatchMetricsDispatcherTest { eq(InternalLogger.Target.MAINTAINER), argThat { this.invoke() == - BatchMetricsDispatcher.WRONG_FILE_NAME_MESSAGE_FORMAT - .format(Locale.ENGLISH, fakeFile.name) + BatchMetricsDispatcher.WRONG_FILE_NAME_MESSAGE_FORMAT + .format(Locale.ENGLISH, fakeFile.name) }, eq(null), eq(false), @@ -584,12 +584,12 @@ internal class BatchMetricsDispatcherTest { BatchMetricsDispatcher.TRACK_KEY to resolveTrackName(fakeFeatureName), BatchMetricsDispatcher.BATCH_AGE_KEY to max(0, (currentTimeInMillis - file.name.toLong())), BatchMetricsDispatcher.UPLOADER_WINDOW_KEY to - fakeFilePersistenceConfig.recentDelayMs, + fakeFilePersistenceConfig.recentDelayMs, BatchMetricsDispatcher.UPLOADER_DELAY_KEY to mapOf( BatchMetricsDispatcher.UPLOADER_DELAY_MIN_KEY to - fakeUploadConfiguration.minDelayMs, + fakeUploadConfiguration.minDelayMs, BatchMetricsDispatcher.UPLOADER_DELAY_MAX_KEY to - fakeUploadConfiguration.maxDelayMs + fakeUploadConfiguration.maxDelayMs ), BatchMetricsDispatcher.FILE_NAME to file.name, BatchMetricsDispatcher.THREAD_NAME to Thread.currentThread().name, @@ -606,9 +606,9 @@ internal class BatchMetricsDispatcherTest { BatchMetricsDispatcher.TYPE_KEY to BatchMetricsDispatcher.BATCH_CLOSED_TYPE_VALUE, BatchMetricsDispatcher.TRACK_KEY to resolveTrackName(fakeFeatureName), BatchMetricsDispatcher.BATCH_DURATION_KEY to - max(0, (batchClosedMetadata.lastTimeWasUsedInMs - file.name.toLong())), + max(0, (batchClosedMetadata.lastTimeWasUsedInMs - file.name.toLong())), BatchMetricsDispatcher.UPLOADER_WINDOW_KEY to - fakeFilePersistenceConfig.recentDelayMs, + fakeFilePersistenceConfig.recentDelayMs, BatchMetricsDispatcher.FORCE_NEW_KEY to batchClosedMetadata.forcedNew, BatchMetricsDispatcher.BATCH_EVENTS_COUNT_KEY to batchClosedMetadata.eventsCount, BatchMetricsDispatcher.FILE_NAME to file.name, diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProviderTest.kt index faf9f72c95..ea048a0e95 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProviderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/BroadcastReceiverNetworkInfoProviderTest.kt @@ -80,7 +80,7 @@ internal class BroadcastReceiverNetworkInfoProviderTest { whenever(mockContext.getSystemService(Context.TELEPHONY_SERVICE)) .doReturn(mockTelephonyManager) whenever(mockConnectivityManager.activeNetworkInfo) doReturn mockNetworkInfo - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.BASE + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.BASE testedProvider = BroadcastReceiverNetworkInfoProvider( mockWriter, @@ -246,7 +246,7 @@ internal class BroadcastReceiverNetworkInfoProviderTest { forge: Forge ) { // GIVEN - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.P + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.P val carrierName = forge.anAlphabeticalString() val carrierId = forge.aPositiveInt(strict = true) @@ -291,7 +291,7 @@ internal class BroadcastReceiverNetworkInfoProviderTest { forge: Forge ) { // GIVEN - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.P + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.P val carrierName = forge.anAlphabeticalString() val carrierId = forge.aPositiveInt(strict = true) @@ -336,7 +336,7 @@ internal class BroadcastReceiverNetworkInfoProviderTest { forge: Forge ) { // GIVEN - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.P + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.P val carrierName = forge.anAlphabeticalString() val carrierId = forge.aPositiveInt(strict = true) @@ -381,7 +381,7 @@ internal class BroadcastReceiverNetworkInfoProviderTest { forge: Forge ) { // GIVEN - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.P + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.P val carrierName = forge.anAlphabeticalString() val carrierId = forge.aPositiveInt(strict = true) @@ -423,7 +423,7 @@ internal class BroadcastReceiverNetworkInfoProviderTest { @MethodSource("getKnownMobileTypes") fun `connected to mobile unknown API 28+`(mobileType: MobileType, forge: Forge) { // GIVEN - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.P + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.P val subtype = forge.anInt(min = 32) val carrierName = forge.anAlphabeticalString() @@ -447,7 +447,7 @@ internal class BroadcastReceiverNetworkInfoProviderTest { @MethodSource("getKnownMobileTypes") fun `connected to mobile unknown carrier`(mobileType: MobileType) { // GIVEN - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.P + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.P stubNetworkInfo( mobileType.id, diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProviderTest.kt index 50db796916..f26dd9441e 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProviderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/net/info/CallbackNetworkInfoProviderTest.kt @@ -70,7 +70,7 @@ internal class CallbackNetworkInfoProviderTest { whenever(mockCapabilities.signalStrength) doReturn NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED whenever(mockCapabilities.hasTransport(any())) doReturn false - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.BASE + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.BASE testedProvider = CallbackNetworkInfoProvider(mockWriter, mockBuildSdkVersionProvider, mockInternalLogger) @@ -100,7 +100,7 @@ internal class CallbackNetworkInfoProviderTest { whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed whenever(mockCapabilities.signalStrength) doReturn strength - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.Q + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.Q // WHEN testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) @@ -171,7 +171,7 @@ internal class CallbackNetworkInfoProviderTest { whenever(mockCapabilities.linkUpstreamBandwidthKbps) doReturn upSpeed whenever(mockCapabilities.linkDownstreamBandwidthKbps) doReturn downSpeed whenever(mockCapabilities.signalStrength) doReturn strength - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.Q + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.Q // WHEN testedProvider.onCapabilitiesChanged(mockNetwork, mockCapabilities) diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/DefaultAndroidInfoProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/DefaultAndroidInfoProviderTest.kt index a7a8d51bcf..bfb9b93a63 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/DefaultAndroidInfoProviderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/system/DefaultAndroidInfoProviderTest.kt @@ -24,6 +24,7 @@ import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assumptions.assumeFalse import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -73,7 +74,7 @@ internal class DefaultAndroidInfoProviderTest { mockUiModeManager whenever(mockContext.getSystemService(Context.TELEPHONY_SERVICE)) doReturn mockTelephonyManager - whenever(mockSdkVersionProvider.version()) doReturn + whenever(mockSdkVersionProvider.version) doReturn forge.anInt(min = Build.VERSION_CODES.BASE) whenever(mockContext.packageManager) doReturn mockPackageManager whenever(mockContext.resources) doReturn mockResources @@ -350,6 +351,7 @@ internal class DefaultAndroidInfoProviderTest { @StringForgery fakeModel: String ) { // Given + assumeFalse(fakeModel.contains(fakeBrand, ignoreCase = true)) Build::class.java.setStaticValue("BRAND", fakeBrand) Build::class.java.setStaticValue("MODEL", fakeModel) testedProvider = createProvider() diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ThreadPoolExecutorExtTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ThreadPoolExecutorExtTest.kt index 9158e77a22..f25b6dfe1b 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ThreadPoolExecutorExtTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/thread/ThreadPoolExecutorExtTest.kt @@ -42,8 +42,7 @@ internal class ThreadPoolExecutorExtTest { @Test fun `M return false W waitToIdle { timeout reached }`( - @LongForgery(min = 0, max = 500) - fakeTimeout: Long, + @LongForgery(min = 0, max = 500) fakeTimeout: Long, forge: Forge ) { // GIVEN @@ -62,8 +61,7 @@ internal class ThreadPoolExecutorExtTest { @Test fun `M wait max timeout milliseconds W waitToIdle { executor not idled }`( - @LongForgery(min = 500, max = 1000) - fakeTimeout: Long, + @LongForgery(min = 500, max = 1000) fakeTimeout: Long, forge: Forge ) { // GIVEN @@ -78,15 +76,13 @@ internal class ThreadPoolExecutorExtTest { } // THEN - assertThat(duration).isCloseTo(fakeTimeout, Offset.offset(100)) + assertThat(duration).isCloseTo(fakeTimeout, Offset.offset(130)) } @Test fun `M return true W waitToIdle { executor idled }`( - @LongForgery(min = 0, max = 500) - fakeTimeout: Long, - @LongForgery(min = 0, max = 10) - fakeTaskCount: Long + @LongForgery(min = 0, max = 500) fakeTimeout: Long, + @LongForgery(min = 0, max = 10) fakeTaskCount: Long ) { // GIVEN @@ -106,10 +102,8 @@ internal class ThreadPoolExecutorExtTest { @LongForgery( min = MAX_SLEEP_DURATION_IN_MS * 3, max = MAX_SLEEP_DURATION_IN_MS * 4 - ) - fakeTimeout: Long, - @LongForgery(min = 0, max = 10) - fakeTaskCount: Long + ) fakeTimeout: Long, + @LongForgery(min = 0, max = 10) fakeTaskCount: Long ) { // GIVEN @@ -126,8 +120,7 @@ internal class ThreadPoolExecutorExtTest { @Test fun `M return false W waitToIdle { timeout is negative, executor not idled }`( - @LongForgery(min = Long.MIN_VALUE, max = 0) - fakeTimeout: Long, + @LongForgery(min = Long.MIN_VALUE, max = 0) fakeTimeout: Long, forge: Forge ) { // WHEN @@ -143,10 +136,8 @@ internal class ThreadPoolExecutorExtTest { @Test fun `M return true W waitToIdle { timeout is negative, executor idled }`( - @LongForgery(min = Long.MIN_VALUE, max = 0) - fakeTimeout: Long, - @LongForgery(min = 0, max = 10) - fakeTaskCount: Long + @LongForgery(min = Long.MIN_VALUE, max = 0) fakeTimeout: Long, + @LongForgery(min = 0, max = 10) fakeTaskCount: Long ) { // GIVEN whenever(testedMockExecutor.taskCount).thenReturn(fakeTaskCount) @@ -165,10 +156,8 @@ internal class ThreadPoolExecutorExtTest { @LongForgery( min = MAX_SLEEP_DURATION_IN_MS * 3, max = MAX_SLEEP_DURATION_IN_MS * 4 - ) - fakeTimeout: Long, - @LongForgery(min = 0, max = 10) - fakeTaskCount: Long + ) fakeTimeout: Long, + @LongForgery(min = 0, max = 10) fakeTaskCount: Long ) { // GIVEN whenever(testedMockExecutor.taskCount) diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/error/internal/DatadogExceptionHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/error/internal/DatadogExceptionHandlerTest.kt index a4081424ab..41df18e54e 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/error/internal/DatadogExceptionHandlerTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/error/internal/DatadogExceptionHandlerTest.kt @@ -161,7 +161,7 @@ internal class DatadogExceptionHandlerTest { val logEvent = lastValue as JvmCrash.Logs - assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(200)) + assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(250)) assertThat(logEvent.threadName).isEqualTo(currentThread.name) assertThat(logEvent.throwable).isSameAs(fakeThrowable) assertThat(logEvent.message).isEqualTo(fakeThrowable.message) @@ -194,7 +194,7 @@ internal class DatadogExceptionHandlerTest { val logEvent = lastValue as JvmCrash.Logs - assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(200)) + assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(250)) assertThat(logEvent.threadName).isEqualTo(currentThread.name) assertThat(logEvent.throwable).isSameAs(fakeThrowable) assertThat(logEvent.message).isEqualTo(fakeThrowable.message) @@ -228,7 +228,7 @@ internal class DatadogExceptionHandlerTest { val logEvent = lastValue as JvmCrash.Logs - assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(200)) + assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(250)) assertThat(logEvent.threadName).isEqualTo(currentThread.name) assertThat(logEvent.throwable).isSameAs(throwable) assertThat(logEvent.message) @@ -263,7 +263,7 @@ internal class DatadogExceptionHandlerTest { val logEvent = lastValue as JvmCrash.Logs - assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(200)) + assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(250)) assertThat(logEvent.threadName).isEqualTo(currentThread.name) assertThat(logEvent.throwable).isSameAs(throwable) assertThat(logEvent.message) @@ -307,7 +307,7 @@ internal class DatadogExceptionHandlerTest { val logEvent = lastValue as JvmCrash.Logs - assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(200)) + assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(250)) assertThat(logEvent.threadName).isEqualTo(thread.name) assertThat(logEvent.throwable).isSameAs(fakeThrowable) assertThat(logEvent.message).isEqualTo(fakeThrowable.message) @@ -348,7 +348,7 @@ internal class DatadogExceptionHandlerTest { val logEvent = lastValue as JvmCrash.Logs - assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(200)) + assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(250)) assertThat(logEvent.threadName).isEqualTo(crashedThread.name) assertThat(logEvent.throwable).isSameAs(fakeThrowable) assertThat(logEvent.message).isEqualTo(fakeThrowable.message) diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandlerTest.kt index 3e70d56d45..519d5cf334 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandlerTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/ndk/internal/DatadogNdkCrashHandlerTest.kt @@ -12,10 +12,8 @@ import com.datadog.android.api.context.UserInfo import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope import com.datadog.android.api.feature.FeatureSdkCore -import com.datadog.android.api.storage.RawBatchEvent import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.persistence.file.FileReader -import com.datadog.android.core.internal.persistence.file.batch.BatchFileReader import com.datadog.android.log.LogAttributes import com.datadog.android.utils.forge.Configurator import com.datadog.android.utils.verifyLog @@ -75,9 +73,6 @@ internal class DatadogNdkCrashHandlerTest { @Mock lateinit var mockNdkCrashLogDeserializer: Deserializer - @Mock - lateinit var mockRumEventDeserializer: Deserializer - @Mock lateinit var mockNetworkInfoDeserializer: Deserializer @@ -97,10 +92,10 @@ internal class DatadogNdkCrashHandlerTest { lateinit var mockInternalLogger: InternalLogger @Mock - lateinit var mockRumFileReader: BatchFileReader + lateinit var mockEnvFileReader: FileReader @Mock - lateinit var mockEnvFileReader: FileReader + lateinit var mockLastRumViewEventProvider: () -> JsonObject? lateinit var fakeNdkCacheDir: File @@ -113,11 +108,6 @@ internal class DatadogNdkCrashHandlerTest { @BeforeEach fun `set up`() { fakeNdkCacheDir = File(tempDir, DatadogNdkCrashHandler.NDK_CRASH_REPORTS_FOLDER_NAME) - whenever(mockRumFileReader.readData(any())) doAnswer { - listOf( - RawBatchEvent(it.getArgument(0).readBytes()) - ) - } whenever(mockEnvFileReader.readData(any())) doAnswer { it.getArgument(0).readBytes() } @@ -134,12 +124,11 @@ internal class DatadogNdkCrashHandlerTest { tempDir, mockExecutorService, mockNdkCrashLogDeserializer, - mockRumEventDeserializer, mockNetworkInfoDeserializer, mockUserInfoDeserializer, mockInternalLogger, - mockRumFileReader, - mockEnvFileReader + mockEnvFileReader, + mockLastRumViewEventProvider ) } @@ -168,16 +157,12 @@ internal class DatadogNdkCrashHandlerTest { @Test fun `𝕄 read last RUM View event 𝕎 prepareData()`( - @StringForgery viewEventStr: String, forge: Forge ) { // Given fakeNdkCacheDir.mkdirs() - File(fakeNdkCacheDir, DatadogNdkCrashHandler.RUM_VIEW_EVENT_FILE_NAME).writeText( - viewEventStr - ) val fakeViewEvent = forge.aFakeViewEvent() - whenever(mockRumEventDeserializer.deserialize(viewEventStr)) doReturn fakeViewEvent.toJson() + whenever(mockLastRumViewEventProvider()) doReturn fakeViewEvent.toJson() // When testedHandler.prepareData() @@ -237,7 +222,6 @@ internal class DatadogNdkCrashHandlerTest { fun `𝕄 do nothing 𝕎 prepareData() {directory does not exist}`() { // When testedHandler.prepareData() - whenever(mockRumEventDeserializer.deserialize(any())) doReturn mock() whenever(mockNdkCrashLogDeserializer.deserialize(any())) doReturn mock() whenever(mockUserInfoDeserializer.deserialize(any())) doReturn mock() whenever(mockNetworkInfoDeserializer.deserialize(any())) doReturn mock() @@ -254,7 +238,6 @@ internal class DatadogNdkCrashHandlerTest { @Test fun `𝕄 clear crash data 𝕎 prepareData()`( @StringForgery crashData: String, - @StringForgery viewEvent: String, @StringForgery networkInfo: String, @StringForgery userInfo: String ) { @@ -262,7 +245,6 @@ internal class DatadogNdkCrashHandlerTest { fakeNdkCacheDir.mkdirs() File(fakeNdkCacheDir, DatadogNdkCrashHandler.CRASH_DATA_FILE_NAME).writeText(crashData) - File(fakeNdkCacheDir, DatadogNdkCrashHandler.RUM_VIEW_EVENT_FILE_NAME).writeText(viewEvent) File(fakeNdkCacheDir, DatadogNdkCrashHandler.NETWORK_INFO_FILE_NAME) .writeText(networkInfo) File(fakeNdkCacheDir, DatadogNdkCrashHandler.USER_INFO_FILE_NAME).writeText(userInfo) @@ -393,13 +375,12 @@ internal class DatadogNdkCrashHandlerTest { tempDir, mockExecutorService, mockNdkCrashLogDeserializer, - mockRumEventDeserializer, mockNetworkInfoDeserializer, mockUserInfoDeserializer, mockInternalLogger, - mockRumFileReader, mockEnvFileReader, - "ndk+il2cpp" + lastRumViewEventProvider = { JsonObject() }, + nativeCrashSourceType = "ndk+il2cpp" ) val fakeViewEvent = forge.aFakeViewEvent() @@ -565,13 +546,12 @@ internal class DatadogNdkCrashHandlerTest { tempDir, mockExecutorService, mockNdkCrashLogDeserializer, - mockRumEventDeserializer, mockNetworkInfoDeserializer, mockUserInfoDeserializer, mockInternalLogger, - mockRumFileReader, mockEnvFileReader, - "ndk+il2cpp" + lastRumViewEventProvider = { JsonObject() }, + nativeCrashSourceType = "ndk+il2cpp" ) val fakeViewEvent = forge.aFakeViewEvent() diff --git a/detekt_custom.yml b/detekt_custom.yml index 7b22716fbb..154a6342ae 100644 --- a/detekt_custom.yml +++ b/detekt_custom.yml @@ -97,6 +97,7 @@ datadog: treatUnknownConstructorAsThrowing: true knownThrowingCalls: # region Android + - "android.app.ActivityManager.getHistoricalProcessExitReasons(kotlin.String?, kotlin.Int, kotlin.Int):java.lang.RuntimeException" - "android.content.pm.PackageManager.getPackageInfo(kotlin.String, android.content.pm.PackageManager.PackageInfoFlags):android.content.pm.PackageManager.NameNotFoundException" - "android.content.pm.PackageManager.getPackageInfo(kotlin.String, kotlin.Int):android.content.pm.PackageManager.NameNotFoundException" - "android.content.res.Resources.getResourceEntryName(kotlin.Int):android.content.res.Resources.NotFoundException" @@ -157,6 +158,8 @@ datadog: - "java.io.InputStream.read(kotlin.ByteArray, kotlin.Int, kotlin.Int):java.io.IOException" - "java.io.InputStream.reset():java.io.IOException" - "java.io.InputStream.skip(kotlin.Long):java.io.IOException" + - "java.io.InputStream.use(kotlin.Function1):java.io.IOException" + - "java.io.InputStreamReader.readText():java.io.IOException" - "java.nio.ByteBuffer.allocate(kotlin.Int):java.lang.IllegalArgumentException" - "java.nio.ByteBuffer.array():java.nio.ReadOnlyBufferException,java.lang.UnsupportedOperationException" - "java.nio.ByteBuffer.put(kotlin.ByteArray):java.nio.BufferOverflowException,java.nio.ReadOnlyBufferException" @@ -193,7 +196,8 @@ datadog: - "java.io.PrintWriter.constructor(java.io.Writer):java.lang.NullPointerException" - "java.lang.Character.toChars(kotlin.Int):java.lang.IllegalArgumentException" - "java.lang.Class.forName(kotlin.String?):java.lang.LinkageError,java.lang.ExceptionInInitializerError,java.lang.ClassNotFoundException" - - "java.lang.Class.getDeclaredField(kotlin.String):java.lang.NoSuchFieldException,java.lang.SecurityException,java.lang.NullPointerException" + - "java.lang.Class.getDeclaredField(kotlin.String?):java.lang.NoSuchFieldException,java.lang.SecurityException,java.lang.NullPointerException" + - "java.lang.reflect.Field.get(kotlin.Any?):java.lang.IllegalArgumentException,java.lang.IllegalAccessException" - "java.lang.Class.getMethod(kotlin.String?, kotlin.Array?):java.lang.NoSuchMethodException,java.lang.SecurityException,java.lang.NullPointerException" - "java.lang.Class.isAssignableFrom(java.lang.Class?):java.lang.NullPointerException" - "java.lang.Runtime.addShutdownHook(java.lang.Thread):java.lang.IllegalArgumentException,java.lang.IllegalStateException,java.lang.SecurityException" @@ -442,6 +446,7 @@ datadog: - "android.graphics.drawable.Drawable.setBounds(kotlin.Int, kotlin.Int, kotlin.Int, kotlin.Int)" - "android.graphics.drawable.LayerDrawable.safeGetDrawable(kotlin.Int, com.datadog.android.api.InternalLogger)" - "android.graphics.drawable.RippleDrawable.safeGetDrawable(kotlin.Int, com.datadog.android.api.InternalLogger)" + - "android.graphics.drawable.RippleDrawable.findIndexByLayerId(kotlin.Int)" - "android.graphics.Point.constructor()" - "android.graphics.Rect.centerX()" - "android.graphics.Rect.centerY()" @@ -693,8 +698,11 @@ datadog: - "java.io.File.safeCall(kotlin.Long, com.datadog.android.api.InternalLogger, kotlin.Function1)" - "java.io.File.safeCall(kotlin.String?, com.datadog.android.api.InternalLogger, kotlin.Function1)" - "java.io.File.safeCall(kotlin.collections.List?, com.datadog.android.api.InternalLogger, kotlin.Function1)" + - "java.io.File.safeCall(kotlin.Unit?, com.datadog.android.api.InternalLogger, kotlin.Function1)" + - "java.io.File.writeTextSafe(kotlin.String, java.nio.charset.Charset, com.datadog.android.api.InternalLogger)" - "java.io.InputStream.mark(kotlin.Int)" - "java.io.InputStream.markSupported()" + - "java.io.InputStream.reader(java.nio.charset.Charset)" - "java.io.StringWriter.constructor()" - "java.io.close()" # endregion @@ -810,6 +818,7 @@ datadog: - "kotlin.collections.List.contains(kotlin.String)" - "kotlin.collections.List.count()" - "kotlin.collections.List.drop(kotlin.Int)" + - "kotlin.collections.List.elementAtOrNull(kotlin.Int)" - "kotlin.collections.List.filter(kotlin.Function1)" - "kotlin.collections.List.filterIndexed(kotlin.Function2)" - "kotlin.collections.List.filterNotNull(kotlin.Function1)" @@ -984,6 +993,7 @@ datadog: - "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.PlaceholderWireframe)" - "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.ShapeWireframe)" - "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.TextWireframe)" + - "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe)" - "kotlin.collections.listOf(java.io.File)" - "kotlin.collections.listOf(kotlin.Array)" - "kotlin.collections.listOf(kotlin.String)" @@ -1119,12 +1129,14 @@ datadog: - "kotlin.String.toMediaTypeOrNull()" - "kotlin.String.toMethod()" - "kotlin.String.toOperationType(com.datadog.android.api.InternalLogger)" + - "kotlin.String.trimStart()" - "kotlin.String.uppercase(java.util.Locale)" - "kotlin.String.withSdkName()" - "kotlin.text.String(kotlin.ByteArray)" - "kotlin.text.String(kotlin.ByteArray, java.nio.charset.Charset)" - "kotlin.text.String(kotlin.CharArray)" - "kotlin.text.StringBuilder()" + - "kotlin.text.buildString(kotlin.Function1)" # endregion # region Kotlin Misc - "kotlin.Any.resolveViewUrl()" @@ -1136,6 +1148,7 @@ datadog: - "kotlin.Throwable.fillInStackTrace()" - "kotlin.Throwable.stackTraceToString()" - "kotlin.ranges.IntRange.reversed()" + - "kotlin.ranges.IntRange.map(kotlin.Function1)" - "kotlin.text.Regex.constructor(kotlin.String)" - "kotlin.text.Regex.matchEntire(kotlin.CharSequence)" # endregion diff --git a/features/dd-sdk-android-logs/api/apiSurface b/features/dd-sdk-android-logs/api/apiSurface index 8f7ee5879b..946e8cade6 100644 --- a/features/dd-sdk-android-logs/api/apiSurface +++ b/features/dd-sdk-android-logs/api/apiSurface @@ -65,7 +65,7 @@ data class com.datadog.android.log.model.LogEvent fun fromJson(kotlin.String): Network fun fromJsonObject(com.google.gson.JsonObject): Network data class Error - constructor(kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.collections.List? = null) + constructor(kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.collections.List? = null) fun toJson(): com.google.gson.JsonElement companion object fun fromJson(kotlin.String): Error diff --git a/features/dd-sdk-android-logs/api/dd-sdk-android-logs.api b/features/dd-sdk-android-logs/api/dd-sdk-android-logs.api index 39a7993267..6c39861a70 100644 --- a/features/dd-sdk-android-logs/api/dd-sdk-android-logs.api +++ b/features/dd-sdk-android-logs/api/dd-sdk-android-logs.api @@ -200,24 +200,27 @@ public final class com/datadog/android/log/model/LogEvent$Device$Companion { public final class com/datadog/android/log/model/LogEvent$Error { public static final field Companion Lcom/datadog/android/log/model/LogEvent$Error$Companion; public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; - public final fun component5 ()Ljava/util/List; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lcom/datadog/android/log/model/LogEvent$Error; - public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$Error;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$Error; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lcom/datadog/android/log/model/LogEvent$Error; + public static synthetic fun copy$default (Lcom/datadog/android/log/model/LogEvent$Error;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/datadog/android/log/model/LogEvent$Error; public fun equals (Ljava/lang/Object;)Z public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/log/model/LogEvent$Error; public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/log/model/LogEvent$Error; + public final fun getFingerprint ()Ljava/lang/String; public final fun getKind ()Ljava/lang/String; public final fun getMessage ()Ljava/lang/String; public final fun getSourceType ()Ljava/lang/String; public final fun getStack ()Ljava/lang/String; public final fun getThreads ()Ljava/util/List; public fun hashCode ()I + public final fun setFingerprint (Ljava/lang/String;)V public final fun setKind (Ljava/lang/String;)V public final fun setMessage (Ljava/lang/String;)V public final fun setSourceType (Ljava/lang/String;)V diff --git a/features/dd-sdk-android-logs/src/main/json/log/log-schema.json b/features/dd-sdk-android-logs/src/main/json/log/log-schema.json index ebe32720b0..297f8d28d0 100644 --- a/features/dd-sdk-android-logs/src/main/json/log/log-schema.json +++ b/features/dd-sdk-android-logs/src/main/json/log/log-schema.json @@ -182,6 +182,11 @@ "description": "The source_type of the error (e.g. 'android', 'flutter', 'react-native')", "readOnly": false }, + "fingerprint": { + "type": "string", + "description": "A custom fingerprint for this error", + "readOnly": false + }, "threads": { "type": "array", "description": "Description of each thread in the process when error happened.", diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/Logs.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/Logs.kt index fc3e2c4065..38e6a848bf 100644 --- a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/Logs.kt +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/Logs.kt @@ -110,5 +110,5 @@ object Logs { internal const val LOGS_NOT_ENABLED_MESSAGE = "You're trying to add attributes to logs, but the feature is not enabled. " + - "Please enable it first." + "Please enable it first." } diff --git a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/DatadogLogGenerator.kt b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/DatadogLogGenerator.kt index a7a3afe2cc..7c95ca2b11 100644 --- a/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/DatadogLogGenerator.kt +++ b/features/dd-sdk-android-logs/src/main/kotlin/com/datadog/android/log/internal/domain/DatadogLogGenerator.kt @@ -44,12 +44,15 @@ internal class DatadogLogGenerator( networkInfo: NetworkInfo?, threads: List ): LogEvent { + val mutableAttributes = attributes.toMutableMap() val error = throwable?.let { + val fingerprint = mutableAttributes.remove(LogAttributes.ERROR_FINGERPRINT) as? String val kind = it.javaClass.canonicalName ?: it.javaClass.simpleName LogEvent.Error( kind = kind, stack = it.stackTraceToString(), message = it.message, + fingerprint = fingerprint, threads = threads.map { thread -> LogEvent.Thread( name = thread.name, @@ -64,7 +67,7 @@ internal class DatadogLogGenerator( level, message, error, - attributes, + mutableAttributes, tags, timestamp, threadName, @@ -99,8 +102,12 @@ internal class DatadogLogGenerator( val mutableAttributes = attributes.toMutableMap() val error = if (errorKind != null || errorMessage != null || errorStack != null) { val sourceType = mutableAttributes.remove(LogAttributes.SOURCE_TYPE) as? String + val fingerprint = mutableAttributes.remove(LogAttributes.ERROR_FINGERPRINT) as? String LogEvent.Error( - kind = errorKind, message = errorMessage, stack = errorStack, + kind = errorKind, + message = errorMessage, + stack = errorStack, + fingerprint = fingerprint, sourceType = sourceType ) } else { diff --git a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/DatadogLogGeneratorTest.kt b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/DatadogLogGeneratorTest.kt index 33a6f0d57a..48b5777789 100644 --- a/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/DatadogLogGeneratorTest.kt +++ b/features/dd-sdk-android-logs/src/test/kotlin/com/datadog/android/log/internal/domain/DatadogLogGeneratorTest.kt @@ -316,7 +316,7 @@ internal class DatadogLogGeneratorTest { } @Test - fun `M note add sourceType W creating the Log { source_type attribute not set }`() { + fun `M not add sourceType W creating the Log { source_type attribute not set }`() { // WHEN val log = testedLogGenerator.generateLog( fakeLevel, @@ -377,6 +377,105 @@ internal class DatadogLogGeneratorTest { assertThat(log.additionalProperties).doesNotContainKey(LogAttributes.SOURCE_TYPE) } + @Test + fun `M not add fingerprint W creating the Log { fingerprint attribute not set }`() { + // WHEN + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + fakeAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasError( + LogEvent.Error( + kind = fakeThrowable.javaClass.canonicalName, + stack = fakeThrowable.stackTraceToString(), + message = fakeThrowable.message, + sourceType = null, + fingerprint = null, + threads = null + ) + ) + } + + @Test + fun `M add fingerprint W creating the Log { fingerprint attribute set }`( + @StringForgery fakeFingerprint: String + ) { + // WHEN + val modifiedAttributes = fakeAttributes.toMutableMap().apply { + put(LogAttributes.ERROR_FINGERPRINT, fakeFingerprint) + } + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable, + modifiedAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasError( + LogEvent.Error( + kind = fakeThrowable.javaClass.canonicalName, + stack = fakeThrowable.stackTraceToString(), + message = fakeThrowable.message, + sourceType = null, + fingerprint = fakeFingerprint, + threads = null + ) + ) + assertThat(log.additionalProperties).doesNotContainKey(LogAttributes.ERROR_FINGERPRINT) + } + + @Test + fun `M add fingerprint W creating the Log { expanded error, fingerprint attribute set }`() { + // WHEN + val modifiedAttributes = fakeAttributes.toMutableMap().apply { + put(LogAttributes.ERROR_FINGERPRINT, "fake_fingerprint") + } + val log = testedLogGenerator.generateLog( + fakeLevel, + fakeLogMessage, + fakeThrowable.javaClass.canonicalName, + fakeThrowable.message, + fakeThrowable.stackTraceToString(), + modifiedAttributes, + fakeTags, + fakeTimestamp, + fakeThreadName, + fakeDatadogContext, + attachNetworkInfo = true, + fakeLoggerName + ) + + // THEN + assertThat(log).hasError( + LogEvent.Error( + kind = fakeThrowable.javaClass.canonicalName, + stack = fakeThrowable.stackTraceToString(), + message = fakeThrowable.message, + sourceType = null, + fingerprint = "fake_fingerprint", + threads = null + ) + ) + assertThat(log.additionalProperties).doesNotContainKey(LogAttributes.ERROR_FINGERPRINT) + } + @Test fun `M add the thread dump W creating the Log`( @Forgery fakeThreads: List diff --git a/features/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashReportsFeature.kt b/features/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashReportsFeature.kt index a788273c8e..2159ca1ed5 100644 --- a/features/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashReportsFeature.kt +++ b/features/dd-sdk-android-ndk/src/main/kotlin/com/datadog/android/ndk/internal/NdkCrashReportsFeature.kt @@ -26,7 +26,7 @@ internal class NdkCrashReportsFeature(private val sdkCore: FeatureSdkCore) : TrackingConsentProviderCallback { private var nativeLibraryLoaded = false - override val name: String = "ndk-crash-reporting" + override val name: String = Feature.NDK_CRASH_REPORTS_FEATURE_NAME // region Feature @Suppress("ReturnCount") diff --git a/features/dd-sdk-android-rum/api/apiSurface b/features/dd-sdk-android-rum/api/apiSurface index ec15f8aee5..9dc1d5da82 100644 --- a/features/dd-sdk-android-rum/api/apiSurface +++ b/features/dd-sdk-android-rum/api/apiSurface @@ -43,6 +43,7 @@ object com.datadog.android.rum.RumAttributes const val ERROR_RESOURCE_URL: String const val ERROR_DATABASE_VERSION: String const val ERROR_DATABASE_PATH: String + const val ERROR_FINGERPRINT: String const val ACTION_TARGET_CLASS_NAME: String const val ACTION_TARGET_TITLE: String const val ACTION_TARGET_PARENT_INDEX: String @@ -69,6 +70,7 @@ data class com.datadog.android.rum.RumConfiguration fun disableUserInteractionTracking(): Builder fun useViewTrackingStrategy(com.datadog.android.rum.tracking.ViewTrackingStrategy?): Builder fun trackLongTasks(Long = RumFeature.DEFAULT_LONG_TASK_THRESHOLD_MS): Builder + fun trackNonFatalAnrs(Boolean): Builder fun setViewEventMapper(com.datadog.android.rum.event.ViewEventMapper): Builder fun setResourceEventMapper(com.datadog.android.event.EventMapper): Builder fun setActionEventMapper(com.datadog.android.event.EventMapper): Builder @@ -636,7 +638,7 @@ data class com.datadog.android.rum.model.ErrorEvent fun fromJson(kotlin.String): Container fun fromJsonObject(com.google.gson.JsonObject): Container data class Error - constructor(kotlin.String? = null, kotlin.String, ErrorSource, kotlin.String? = null, kotlin.collections.List? = null, kotlin.Boolean? = null, kotlin.String? = null, kotlin.String? = null, Handling? = null, kotlin.String? = null, SourceType? = null, Resource? = null, kotlin.collections.List? = null, kotlin.collections.List? = null, kotlin.Boolean? = null, Meta? = null) + constructor(kotlin.String? = null, kotlin.String, ErrorSource, kotlin.String? = null, kotlin.collections.List? = null, kotlin.Boolean? = null, kotlin.String? = null, kotlin.String? = null, Category? = null, Handling? = null, kotlin.String? = null, SourceType? = null, Resource? = null, kotlin.collections.List? = null, kotlin.collections.List? = null, kotlin.Boolean? = null, Meta? = null) fun toJson(): com.google.gson.JsonElement companion object fun fromJson(kotlin.String): Error @@ -783,6 +785,14 @@ data class com.datadog.android.rum.model.ErrorEvent fun toJson(): com.google.gson.JsonElement companion object fun fromJson(kotlin.String): ErrorSource + enum Category + constructor(kotlin.String) + - ANR + - APP_HANG + - EXCEPTION + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Category enum Handling constructor(kotlin.String) - HANDLED diff --git a/features/dd-sdk-android-rum/api/dd-sdk-android-rum.api b/features/dd-sdk-android-rum/api/dd-sdk-android-rum.api index 838d389085..a11df09844 100644 --- a/features/dd-sdk-android-rum/api/dd-sdk-android-rum.api +++ b/features/dd-sdk-android-rum/api/dd-sdk-android-rum.api @@ -55,6 +55,7 @@ public final class com/datadog/android/rum/RumAttributes { public static final field ENV Ljava/lang/String; public static final field ERROR_DATABASE_PATH Ljava/lang/String; public static final field ERROR_DATABASE_VERSION Ljava/lang/String; + public static final field ERROR_FINGERPRINT Ljava/lang/String; public static final field ERROR_RESOURCE_METHOD Ljava/lang/String; public static final field ERROR_RESOURCE_STATUS_CODE Ljava/lang/String; public static final field ERROR_RESOURCE_URL Ljava/lang/String; @@ -111,6 +112,7 @@ public final class com/datadog/android/rum/RumConfiguration$Builder { public final fun trackLongTasks ()Lcom/datadog/android/rum/RumConfiguration$Builder; public final fun trackLongTasks (J)Lcom/datadog/android/rum/RumConfiguration$Builder; public static synthetic fun trackLongTasks$default (Lcom/datadog/android/rum/RumConfiguration$Builder;JILjava/lang/Object;)Lcom/datadog/android/rum/RumConfiguration$Builder; + public final fun trackNonFatalAnrs (Z)Lcom/datadog/android/rum/RumConfiguration$Builder; public final fun trackUserInteractions ()Lcom/datadog/android/rum/RumConfiguration$Builder; public final fun trackUserInteractions ([Lcom/datadog/android/rum/tracking/ViewAttributesProvider;)Lcom/datadog/android/rum/RumConfiguration$Builder; public final fun trackUserInteractions ([Lcom/datadog/android/rum/tracking/ViewAttributesProvider;Lcom/datadog/android/rum/tracking/InteractionPredicate;)Lcom/datadog/android/rum/RumConfiguration$Builder; @@ -1358,6 +1360,21 @@ public final class com/datadog/android/rum/model/ErrorEvent$BinaryImage$Companio public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/rum/model/ErrorEvent$BinaryImage; } +public final class com/datadog/android/rum/model/ErrorEvent$Category : java/lang/Enum { + public static final field ANR Lcom/datadog/android/rum/model/ErrorEvent$Category; + public static final field APP_HANG Lcom/datadog/android/rum/model/ErrorEvent$Category; + public static final field Companion Lcom/datadog/android/rum/model/ErrorEvent$Category$Companion; + public static final field EXCEPTION Lcom/datadog/android/rum/model/ErrorEvent$Category; + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/rum/model/ErrorEvent$Category; + public final fun toJson ()Lcom/google/gson/JsonElement; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/rum/model/ErrorEvent$Category; + public static fun values ()[Lcom/datadog/android/rum/model/ErrorEvent$Category; +} + +public final class com/datadog/android/rum/model/ErrorEvent$Category$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/rum/model/ErrorEvent$Category; +} + public final class com/datadog/android/rum/model/ErrorEvent$Cause { public static final field Companion Lcom/datadog/android/rum/model/ErrorEvent$Cause$Companion; public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$ErrorSource;)V @@ -1689,16 +1706,17 @@ public final class com/datadog/android/rum/model/ErrorEvent$EffectiveType$Compan public final class com/datadog/android/rum/model/ErrorEvent$Error { public static final field Companion Lcom/datadog/android/rum/model/ErrorEvent$Error$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$ErrorSource;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$Handling;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$SourceType;Lcom/datadog/android/rum/model/ErrorEvent$Resource;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Lcom/datadog/android/rum/model/ErrorEvent$Meta;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$ErrorSource;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$Handling;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$SourceType;Lcom/datadog/android/rum/model/ErrorEvent$Resource;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Lcom/datadog/android/rum/model/ErrorEvent$Meta;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$ErrorSource;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$Category;Lcom/datadog/android/rum/model/ErrorEvent$Handling;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$SourceType;Lcom/datadog/android/rum/model/ErrorEvent$Resource;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Lcom/datadog/android/rum/model/ErrorEvent$Meta;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$ErrorSource;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$Category;Lcom/datadog/android/rum/model/ErrorEvent$Handling;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$SourceType;Lcom/datadog/android/rum/model/ErrorEvent$Resource;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Lcom/datadog/android/rum/model/ErrorEvent$Meta;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; - public final fun component10 ()Ljava/lang/String; - public final fun component11 ()Lcom/datadog/android/rum/model/ErrorEvent$SourceType; - public final fun component12 ()Lcom/datadog/android/rum/model/ErrorEvent$Resource; - public final fun component13 ()Ljava/util/List; + public final fun component10 ()Lcom/datadog/android/rum/model/ErrorEvent$Handling; + public final fun component11 ()Ljava/lang/String; + public final fun component12 ()Lcom/datadog/android/rum/model/ErrorEvent$SourceType; + public final fun component13 ()Lcom/datadog/android/rum/model/ErrorEvent$Resource; public final fun component14 ()Ljava/util/List; - public final fun component15 ()Ljava/lang/Boolean; - public final fun component16 ()Lcom/datadog/android/rum/model/ErrorEvent$Meta; + public final fun component15 ()Ljava/util/List; + public final fun component16 ()Ljava/lang/Boolean; + public final fun component17 ()Lcom/datadog/android/rum/model/ErrorEvent$Meta; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Lcom/datadog/android/rum/model/ErrorEvent$ErrorSource; public final fun component4 ()Ljava/lang/String; @@ -1706,13 +1724,14 @@ public final class com/datadog/android/rum/model/ErrorEvent$Error { public final fun component6 ()Ljava/lang/Boolean; public final fun component7 ()Ljava/lang/String; public final fun component8 ()Ljava/lang/String; - public final fun component9 ()Lcom/datadog/android/rum/model/ErrorEvent$Handling; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$ErrorSource;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$Handling;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$SourceType;Lcom/datadog/android/rum/model/ErrorEvent$Resource;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Lcom/datadog/android/rum/model/ErrorEvent$Meta;)Lcom/datadog/android/rum/model/ErrorEvent$Error; - public static synthetic fun copy$default (Lcom/datadog/android/rum/model/ErrorEvent$Error;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$ErrorSource;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$Handling;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$SourceType;Lcom/datadog/android/rum/model/ErrorEvent$Resource;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Lcom/datadog/android/rum/model/ErrorEvent$Meta;ILjava/lang/Object;)Lcom/datadog/android/rum/model/ErrorEvent$Error; + public final fun component9 ()Lcom/datadog/android/rum/model/ErrorEvent$Category; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$ErrorSource;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$Category;Lcom/datadog/android/rum/model/ErrorEvent$Handling;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$SourceType;Lcom/datadog/android/rum/model/ErrorEvent$Resource;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Lcom/datadog/android/rum/model/ErrorEvent$Meta;)Lcom/datadog/android/rum/model/ErrorEvent$Error; + public static synthetic fun copy$default (Lcom/datadog/android/rum/model/ErrorEvent$Error;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$ErrorSource;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$Category;Lcom/datadog/android/rum/model/ErrorEvent$Handling;Ljava/lang/String;Lcom/datadog/android/rum/model/ErrorEvent$SourceType;Lcom/datadog/android/rum/model/ErrorEvent$Resource;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Lcom/datadog/android/rum/model/ErrorEvent$Meta;ILjava/lang/Object;)Lcom/datadog/android/rum/model/ErrorEvent$Error; public fun equals (Ljava/lang/Object;)Z public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/rum/model/ErrorEvent$Error; public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/rum/model/ErrorEvent$Error; public final fun getBinaryImages ()Ljava/util/List; + public final fun getCategory ()Lcom/datadog/android/rum/model/ErrorEvent$Category; public final fun getCauses ()Ljava/util/List; public final fun getFingerprint ()Ljava/lang/String; public final fun getHandling ()Lcom/datadog/android/rum/model/ErrorEvent$Handling; diff --git a/features/dd-sdk-android-rum/src/main/json/rum/error-schema.json b/features/dd-sdk-android-rum/src/main/json/rum/error-schema.json index c39a04e8ec..1c9f9ff87e 100644 --- a/features/dd-sdk-android-rum/src/main/json/rum/error-schema.json +++ b/features/dd-sdk-android-rum/src/main/json/rum/error-schema.json @@ -98,6 +98,12 @@ "description": "The type of the error", "readOnly": true }, + "category": { + "type": "string", + "description": "The specific category of the error. It provides a high-level grouping for different types of errors.", + "enum": ["ANR", "App Hang", "Exception"], + "readOnly": true + }, "handling": { "type": "string", "description": "Whether the error has been handled manually in the source code or not", 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 965fb72c3a..9576ecbd4a 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 @@ -6,6 +6,7 @@ package com.datadog.android.rum +import android.os.Build import android.os.Handler import android.os.Looper import com.datadog.android.Datadog @@ -72,6 +73,15 @@ object Rum { sdkCore.registerFeature(rumFeature) val rumMonitor = createMonitor(sdkCore, rumFeature) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // small hack: we need to read last RUM view event file, but we don't want to do on the + // main thread, but at the same time we want to read it surely before it is updated + // by the new RUM session, so we will read it on the RUM events thread (once read we + // will switch to another worker thread, so that RUM events thread is not busy) + rumFeature.consumeLastFatalAnr(rumMonitor.executorService) + } + GlobalRumMonitor.registerIfAbsent( monitor = rumMonitor, sdkCore diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt index a979eab11d..d226002597 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt @@ -75,6 +75,11 @@ object RumAttributes { */ const val INTERNAL_ERROR_IS_CRASH: String = "_dd.error.is_crash" + /** + * All threads information. + */ + internal const val INTERNAL_ALL_THREADS: String = "_dd.error.threads" + // endregion // region Resource @@ -159,6 +164,11 @@ object RumAttributes { */ const val ERROR_DATABASE_PATH: String = "error.database.path" + /** + * Specifies a custom error fingerprint for the supplied error. + */ + const val ERROR_FINGERPRINT: String = "_dd.error.fingerprint" + // endregion // region Action diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumConfiguration.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumConfiguration.kt index 8a78fdbe9f..5b7b52d080 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumConfiguration.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumConfiguration.kt @@ -133,6 +133,21 @@ data class RumConfiguration internal constructor( return this } + /** + * Enable tracking of non-fatal ANRs. This is enabled by default on Android API 29 and + * below, and disabled by default on Android API 30 and above. Android API 30+ has a + * capability to report fatal ANRs (always enabled). Please note, that tracking non-fatal + * ANRs is using Watchdog thread approach, which can be noisy, and also leads to ANR + * duplication on Android 30+ if fatal ANR happened, because Watchdog thread approach cannot + * categorize ANR as fatal or non-fatal. + * + * @param enabled whether tracking of non-fatal ANRs is enabled or not. + */ + fun trackNonFatalAnrs(enabled: Boolean): Builder { + rumConfig = rumConfig.copy(trackNonFatalAnrs = enabled) + return this + } + /** * Sets the [ViewEventMapper] for the RUM [ViewEvent]. You can use this interface implementation * to modify the [ViewEvent] attributes before serialisation. diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumMonitor.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumMonitor.kt index deb4526a06..e2d66d3ae9 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumMonitor.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumMonitor.kt @@ -179,7 +179,9 @@ interface RumMonitor { * @param source the source of the error * @param throwable the throwable * @param attributes additional custom attributes to attach to the error. Attributes can be - * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. + * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. Users + * that want to supply a custom fingerprint for this error can add a value under the key + * [RumAttributes.ERROR_CUSTOM_FINGERPRINT] * @see [startResource] * @see [stopResource] */ @@ -205,7 +207,9 @@ interface RumMonitor { * @param errorType the type of the error. Usually it should be the canonical name of the * of the Exception class. * @param attributes additional custom attributes to attach to the error. Attributes can be - * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. + * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. Users + * that want to supply a custom fingerprint for this error can add a value under the key + * [RumAttributes.ERROR_CUSTOM_FINGERPRINT] * @see [startResource] * @see [stopResource] */ @@ -226,7 +230,9 @@ interface RumMonitor { * @param source the source of the error * @param throwable the throwable * @param attributes additional custom attributes to attach to the error. Attributes can be - * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. + * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. Users + * that want to supply a custom fingerprint for this error can add a value under the key + * [RumAttributes.ERROR_CUSTOM_FINGERPRINT] */ fun addError( message: String, @@ -245,7 +251,9 @@ interface RumMonitor { * @param message a message explaining the error * @param source the source of the error * @param stacktrace the error stacktrace information - * @param attributes additional custom attributes to attach to the error + * @param attributes additional custom attributes to attach to the error. Users + * that want to supply a custom fingerprint for this error can add a value under the key + * [RumAttributes.ERROR_CUSTOM_FINGERPRINT] */ fun addErrorWithStacktrace( message: String, diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/ndk/DatadogNdkCrashEventHandler.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/DatadogLateCrashReporter.kt similarity index 54% rename from features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/ndk/DatadogNdkCrashEventHandler.kt rename to features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/DatadogLateCrashReporter.kt index 3d29d29e4d..e5bd6853fa 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/ndk/DatadogNdkCrashEventHandler.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/DatadogLateCrashReporter.kt @@ -4,14 +4,22 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.rum.internal.ndk +package com.datadog.android.rum.internal +import android.app.ApplicationExitInfo +import android.os.Build +import androidx.annotation.RequiresApi import com.datadog.android.api.InternalLogger import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.feature.Feature -import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.api.storage.DataWriter +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.feature.event.ThreadDump import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.rum.internal.anr.ANRDetectorRunnable +import com.datadog.android.rum.internal.anr.ANRException +import com.datadog.android.rum.internal.anr.AndroidTraceParser +import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.event.RumEventDeserializer import com.datadog.android.rum.internal.domain.scope.toErrorSchemaType import com.datadog.android.rum.internal.domain.scope.tryFromSource @@ -20,17 +28,25 @@ import com.datadog.android.rum.model.ViewEvent import com.google.gson.JsonObject import java.util.concurrent.TimeUnit -internal class DatadogNdkCrashEventHandler( - private val internalLogger: InternalLogger, - private val rumEventDeserializer: Deserializer = RumEventDeserializer(internalLogger) -) : NdkCrashEventHandler { +internal class DatadogLateCrashReporter( + private val sdkCore: InternalSdkCore, + private val rumEventDeserializer: Deserializer = RumEventDeserializer( + sdkCore.internalLogger + ), + private val androidTraceParser: AndroidTraceParser = AndroidTraceParser(sdkCore.internalLogger) +) : LateCrashReporter { + + // region LateCrashEventHandler @Suppress("ComplexCondition") - override fun handleEvent(event: Map<*, *>, sdkCore: FeatureSdkCore, rumWriter: DataWriter) { + override fun handleNdkCrashEvent( + event: Map<*, *>, + rumWriter: DataWriter + ) { val rumFeature = sdkCore.getFeature(Feature.RUM_FEATURE_NAME) if (rumFeature == null) { - internalLogger.log( + sdkCore.internalLogger.log( InternalLogger.Level.INFO, InternalLogger.Target.USER, { INFO_RUM_FEATURE_NOT_REGISTERED } @@ -47,12 +63,10 @@ internal class DatadogNdkCrashEventHandler( rumEventDeserializer.deserialize(it) as? ViewEvent } - val sampleRate = lastViewEvent?.dd?.configuration?.sessionSampleRate?.toFloat() ?: 0f - if (timestamp == null || signalName == null || stacktrace == null || errorLogMessage == null || lastViewEvent == null ) { - internalLogger.log( + sdkCore.internalLogger.log( InternalLogger.Level.WARN, InternalLogger.Target.USER, { NDK_CRASH_EVENT_MISSING_MANDATORY_FIELDS } @@ -60,22 +74,21 @@ internal class DatadogNdkCrashEventHandler( return } - val now = System.currentTimeMillis() rumFeature.withWriteContext { datadogContext, eventBatchWriter -> val toSendErrorEvent = resolveErrorEventFromViewEvent( datadogContext, - sourceType, + ErrorEvent.SourceType.tryFromSource(sourceType), + ErrorEvent.Category.EXCEPTION, errorLogMessage, timestamp, stacktrace, signalName, - lastViewEvent, - sampleRate + null, + lastViewEvent ) @Suppress("ThreadSafety") // called in a worker thread context rumWriter.write(eventBatchWriter, toSendErrorEvent) - val sessionsTimeDifference = now - lastViewEvent.date - if (sessionsTimeDifference < VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD) { + if (lastViewEvent.isWithinSessionAvailability) { val updatedViewEvent = updateViewEvent(lastViewEvent) @Suppress("ThreadSafety") // called in a worker thread context rumWriter.write(eventBatchWriter, updatedViewEvent) @@ -83,21 +96,83 @@ internal class DatadogNdkCrashEventHandler( } } + @RequiresApi(Build.VERSION_CODES.R) + override fun handleAnrCrash( + anrExitInfo: ApplicationExitInfo, + lastRumViewEventJson: JsonObject, + rumWriter: DataWriter + ) { + val lastViewEvent = + rumEventDeserializer.deserialize(lastRumViewEventJson) as? ViewEvent ?: return + + val lastKnownViewStartedAt = lastViewEvent.date + if (anrExitInfo.timestamp > lastKnownViewStartedAt) { + val rumFeature = sdkCore.getFeature(Feature.RUM_FEATURE_NAME) + + if (rumFeature == null) { + sdkCore.internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { INFO_RUM_FEATURE_NOT_REGISTERED } + ) + return + } + + rumFeature.withWriteContext { datadogContext, eventBatchWriter -> + // means we are too late, last view event belongs to the ongoing session + if (lastViewEvent.session.id == datadogContext.rumSessionId) return@withWriteContext + + val lastFatalAnrSent = sdkCore.lastFatalAnrSent + if (anrExitInfo.timestamp == lastFatalAnrSent) return@withWriteContext + + val threadDumps = readThreadsDump(anrExitInfo) + if (threadDumps.isEmpty()) return@withWriteContext + + val toSendErrorEvent = resolveErrorEventFromViewEvent( + datadogContext, + ErrorEvent.SourceType.ANDROID, + ErrorEvent.Category.ANR, + ANRDetectorRunnable.ANR_MESSAGE, + anrExitInfo.timestamp, + threadDumps.mainThread?.stack.orEmpty(), + ANRException::class.java.canonicalName.orEmpty(), + threadDumps, + lastViewEvent + ) + @Suppress("ThreadSafety") // called in a worker thread context + rumWriter.write(eventBatchWriter, toSendErrorEvent) + if (lastViewEvent.isWithinSessionAvailability) { + val updatedViewEvent = updateViewEvent(lastViewEvent) + @Suppress("ThreadSafety") // called in a worker thread context + rumWriter.write(eventBatchWriter, updatedViewEvent) + } + @Suppress("ThreadSafety") // called in a worker thread context + sdkCore.writeLastFatalAnrSent(anrExitInfo.timestamp) + } + } + } + + // endregion + + // region Internal + @Suppress("LongMethod", "LongParameterList") private fun resolveErrorEventFromViewEvent( datadogContext: DatadogContext, - sourceTypeStr: String?, + sourceType: ErrorEvent.SourceType, + category: ErrorEvent.Category, errorLogMessage: String, timestamp: Long, stacktrace: String, - signalName: String, - viewEvent: ViewEvent, - sampleRate: Float + errorType: String, + threadDumps: List?, + viewEvent: ViewEvent ): ErrorEvent { val connectivity = viewEvent.connectivity?.let { val connectivityStatus = ErrorEvent.Status.valueOf(it.status.name) - val connectivityInterfaces = it.interfaces?.map { ErrorEvent.Interface.valueOf(it.name) } + val connectivityInterfaces = + it.interfaces?.map { ErrorEvent.Interface.valueOf(it.name) } val cellular = ErrorEvent.Cellular( it.cellular?.technology, it.cellular?.carrierName @@ -111,21 +186,6 @@ internal class DatadogNdkCrashEventHandler( user?.email != null || additionalUserProperties.isNotEmpty() val deviceInfo = datadogContext.deviceInfo - val sourceType = sourceTypeStr?.let { - @Suppress("TooGenericExceptionCaught") - try { - ErrorEvent.SourceType.fromJson(sourceTypeStr) - } catch (e: Exception) { - internalLogger.log( - InternalLogger.Level.ERROR, - InternalLogger.Target.TELEMETRY, - { "Error parsing source type from NDK crash event: $sourceTypeStr" }, - e - ) - ErrorEvent.SourceType.NDK - } - } ?: ErrorEvent.SourceType.NDK - return ErrorEvent( date = timestamp + datadogContext.time.serverTimeOffsetMs, application = ErrorEvent.Application(viewEvent.application.id), @@ -137,7 +197,7 @@ internal class DatadogNdkCrashEventHandler( source = viewEvent.source?.toJson()?.asString?.let { ErrorEvent.ErrorEventSource.tryFromSource( it, - internalLogger + sdkCore.internalLogger ) }, view = ErrorEvent.ErrorEventView( @@ -171,7 +231,7 @@ internal class DatadogNdkCrashEventHandler( ), dd = ErrorEvent.Dd( session = ErrorEvent.DdSession(plan = ErrorEvent.Plan.PLAN_1), - configuration = ErrorEvent.Configuration(sessionSampleRate = sampleRate) + configuration = ErrorEvent.Configuration(sessionSampleRate = viewEvent.sampleRate) ), context = ErrorEvent.Context(additionalProperties = additionalProperties), error = ErrorEvent.Error( @@ -179,13 +239,37 @@ internal class DatadogNdkCrashEventHandler( source = ErrorEvent.ErrorSource.SOURCE, stack = stacktrace, isCrash = true, - type = signalName, - sourceType = sourceType + type = errorType, + sourceType = sourceType, + category = category, + threads = threadDumps?.map { + ErrorEvent.Thread( + it.name, + it.crashed, + it.stack, + it.state + ) + } ), version = viewEvent.version ) } + @RequiresApi(Build.VERSION_CODES.R) + private fun readThreadsDump(anrExitInfo: ApplicationExitInfo): List { + val traceInputStream = anrExitInfo.traceInputStream + if (traceInputStream == null) { + sdkCore.internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { MISSING_ANR_TRACE } + ) + return emptyList() + } + + return androidTraceParser.parse(traceInputStream) + } + private fun updateViewEvent(lastViewEvent: ViewEvent): ViewEvent { val currentCrash = lastViewEvent.view.crash val newCrash = currentCrash?.copy(count = currentCrash.count + 1) ?: ViewEvent.Crash(1) @@ -200,6 +284,43 @@ internal class DatadogNdkCrashEventHandler( ) } + private val ViewEvent.sampleRate: Float + get() = dd.configuration?.sessionSampleRate?.toFloat() ?: 0f + + private val ViewEvent.isWithinSessionAvailability: Boolean + get() { + val now = System.currentTimeMillis() + val sessionsTimeDifference = now - this.date + return sessionsTimeDifference < VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD + } + + private val List.mainThread: ThreadDump? + get() = firstOrNull { it.name == "main" } + + private fun ErrorEvent.SourceType.Companion.tryFromSource( + sourceType: String? + ): ErrorEvent.SourceType { + return if (sourceType != null) { + try { + ErrorEvent.SourceType.fromJson(sourceType) + } catch (e: NoSuchElementException) { + sdkCore.internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.TELEMETRY, + { "Error parsing source type from NDK crash event: $sourceType" }, + e + ) + ErrorEvent.SourceType.NDK + } + } else { + ErrorEvent.SourceType.NDK + } + } + + private val DatadogContext.rumSessionId: String? + get() = featuresContext[Feature.RUM_FEATURE_NAME] + .orEmpty()[RumContext.SESSION_ID] as? String + // endregion companion object { @@ -209,6 +330,8 @@ internal class DatadogNdkCrashEventHandler( "RUM feature received a NDK crash event" + " where one or more mandatory (timestamp, signalName, stacktrace," + " message, lastViewEvent) fields are either missing or have wrong type." + internal const val MISSING_ANR_TRACE = "Last known exit reason has no trace information" + + " attached, cannot report fatal ANR." internal val VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD = TimeUnit.HOURS.toMillis(4) } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/DefaultAppStartTimeProvider.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/DefaultAppStartTimeProvider.kt index af4b31acd5..f887916407 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/DefaultAppStartTimeProvider.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/DefaultAppStartTimeProvider.kt @@ -11,17 +11,16 @@ import android.os.Build import android.os.Process import android.os.SystemClock import com.datadog.android.core.internal.system.BuildSdkVersionProvider -import com.datadog.android.core.internal.system.DefaultBuildSdkVersionProvider import java.util.concurrent.TimeUnit internal class DefaultAppStartTimeProvider( - buildSdkVersionProvider: BuildSdkVersionProvider = DefaultBuildSdkVersionProvider() + buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT ) : AppStartTimeProvider { override val appStartTimeNs: Long by lazy(LazyThreadSafetyMode.PUBLICATION) { @SuppressLint("NewApi") when { - buildSdkVersionProvider.version() >= Build.VERSION_CODES.N -> { + buildSdkVersionProvider.version >= Build.VERSION_CODES.N -> { val diffMs = SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime() System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(diffMs) } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/LateCrashReporter.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/LateCrashReporter.kt new file mode 100644 index 0000000000..243dc74eba --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/LateCrashReporter.kt @@ -0,0 +1,25 @@ +/* + * 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 + +import android.app.ApplicationExitInfo +import android.os.Build +import androidx.annotation.RequiresApi +import com.datadog.android.api.storage.DataWriter +import com.google.gson.JsonObject + +internal interface LateCrashReporter { + + fun handleNdkCrashEvent(event: Map<*, *>, rumWriter: DataWriter) + + @RequiresApi(Build.VERSION_CODES.R) + fun handleAnrCrash( + anrExitInfo: ApplicationExitInfo, + lastRumViewEventJson: JsonObject, + rumWriter: DataWriter + ) +} 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 f90f9daf03..e6ef0bb4c5 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 @@ -6,12 +6,15 @@ package com.datadog.android.rum.internal +import android.app.ActivityManager import android.app.Application +import android.app.ApplicationExitInfo import android.content.Context import android.os.Build import android.os.Handler import android.os.Looper import androidx.annotation.AnyThread +import androidx.annotation.RequiresApi import com.datadog.android.api.InternalLogger import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureEventReceiver @@ -22,9 +25,11 @@ import com.datadog.android.api.storage.DataWriter import com.datadog.android.api.storage.FeatureStorageConfiguration import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.feature.event.JvmCrash +import com.datadog.android.core.internal.system.BuildSdkVersionProvider import com.datadog.android.core.internal.thread.LoggingScheduledThreadPoolExecutor import com.datadog.android.core.internal.utils.executeSafe import com.datadog.android.core.internal.utils.scheduleSafe +import com.datadog.android.core.internal.utils.submitSafe import com.datadog.android.event.EventMapper import com.datadog.android.event.MapperSerializer import com.datadog.android.event.NoOpEventMapper @@ -46,8 +51,6 @@ import com.datadog.android.rum.internal.instrumentation.UserActionTrackingStrate import com.datadog.android.rum.internal.instrumentation.gestures.DatadogGesturesTracker import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor import com.datadog.android.rum.internal.monitor.DatadogRumMonitor -import com.datadog.android.rum.internal.ndk.DatadogNdkCrashEventHandler -import com.datadog.android.rum.internal.ndk.NdkCrashEventHandler import com.datadog.android.rum.internal.net.RumRequestFactory import com.datadog.android.rum.internal.storage.NoOpDataWriter import com.datadog.android.rum.internal.thread.NoOpScheduledExecutorService @@ -79,6 +82,7 @@ import com.datadog.android.rum.tracking.ViewTrackingStrategy import com.datadog.android.telemetry.internal.Telemetry import com.datadog.android.telemetry.internal.TelemetryCoreConfiguration import com.datadog.android.telemetry.model.TelemetryConfigurationEvent +import java.lang.RuntimeException import java.util.Locale import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -91,12 +95,12 @@ import java.util.concurrent.atomic.AtomicReference * RUM feature class, which needs to be registered with Datadog SDK instance. */ @Suppress("TooManyFunctions") -internal class RumFeature constructor( +internal class RumFeature( private val sdkCore: FeatureSdkCore, internal val applicationId: String, internal val configuration: Configuration, - private val ndkCrashEventHandlerFactory: (InternalLogger) -> NdkCrashEventHandler = { - DatadogNdkCrashEventHandler(it) + private val lateCrashReporterFactory: (InternalSdkCore) -> LateCrashReporter = { + DatadogLateCrashReporter(it) } ) : StorageBackedFeature, FeatureEventReceiver { @@ -124,13 +128,12 @@ internal class RumFeature constructor( internal var sessionListener: RumSessionListener = NoOpRumSessionListener() internal var vitalExecutorService: ScheduledExecutorService = NoOpScheduledExecutorService() - internal lateinit var anrDetectorExecutorService: ExecutorService - internal lateinit var anrDetectorRunnable: ANRDetectorRunnable - internal lateinit var anrDetectorHandler: Handler + private var anrDetectorExecutorService: ExecutorService? = null + internal var anrDetectorRunnable: ANRDetectorRunnable? = null internal lateinit var appContext: Context internal lateinit var telemetry: Telemetry - private val ndkCrashEventHandler by lazy { ndkCrashEventHandlerFactory(sdkCore.internalLogger) } + private val lateCrashEventHandler by lazy { lateCrashReporterFactory(sdkCore as InternalSdkCore) } // region Feature @@ -174,7 +177,9 @@ internal class RumFeature constructor( initializeVitalMonitors(configuration.vitalsMonitorUpdateFrequency) - initializeANRDetector() + if (configuration.trackNonFatalAnrs) { + initializeANRDetector() + } registerTrackingStrategies(appContext) @@ -214,8 +219,8 @@ internal class RumFeature constructor( frameRateVitalMonitor = NoOpVitalMonitor() vitalExecutorService.shutdownNow() - anrDetectorExecutorService.shutdownNow() - anrDetectorRunnable.stop() + anrDetectorExecutorService?.shutdownNow() + anrDetectorRunnable?.stop() vitalExecutorService = NoOpScheduledExecutorService() sessionListener = NoOpRumSessionListener() @@ -263,7 +268,7 @@ internal class RumFeature constructor( when (event["type"]) { NDK_CRASH_BUS_MESSAGE_TYPE -> - ndkCrashEventHandler.handleEvent(event, sdkCore, dataWriter) + lateCrashEventHandler.handleNdkCrashEvent(event, dataWriter) LOGGER_ERROR_BUS_MESSAGE_TYPE -> addLoggerError(event) LOGGER_ERROR_WITH_STACK_TRACE_MESSAGE_TYPE -> addLoggerErrorWithStacktrace(event) WEB_VIEW_INGESTED_NOTIFICATION_MESSAGE_TYPE -> { @@ -327,6 +332,43 @@ internal class RumFeature constructor( } } + @RequiresApi(Build.VERSION_CODES.R) + internal fun consumeLastFatalAnr(rumEventsExecutorService: ExecutorService) { + val activityManager = + appContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val lastKnownAnr = try { + activityManager.getHistoricalProcessExitReasons(null, 0, 0) + // from docs: Returns: a list of ApplicationExitInfo records matching the criteria, + // sorted in the order from most recent to least recent. + .firstOrNull { it.reason == ApplicationExitInfo.REASON_ANR } + } catch (@Suppress("TooGenericExceptionCaught") e: RuntimeException) { + sdkCore.internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { FAILED_TO_GET_HISTORICAL_EXIT_REASONS }, + e + ) + null + } ?: return + + rumEventsExecutorService.submitSafe("Send fatal ANR", sdkCore.internalLogger) { + val lastRumViewEvent = (sdkCore as InternalSdkCore).lastViewEvent + if (lastRumViewEvent != null) { + lateCrashEventHandler.handleAnrCrash( + lastKnownAnr, + lastRumViewEvent, + dataWriter + ) + } else { + sdkCore.internalLogger.log( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + { NO_LAST_RUM_VIEW_EVENT_AVAILABLE } + ) + } + } + } + private fun registerTrackingStrategies(appContext: Context) { actionTrackingStrategy.register(sdkCore, appContext) viewTrackingStrategy.register(sdkCore, appContext) @@ -395,14 +437,14 @@ internal class RumFeature constructor( } private fun initializeANRDetector() { - anrDetectorHandler = Handler(Looper.getMainLooper()) - anrDetectorRunnable = ANRDetectorRunnable(sdkCore, anrDetectorHandler) + val detectorRunnable = ANRDetectorRunnable(sdkCore, Handler(Looper.getMainLooper())) anrDetectorExecutorService = Executors.newSingleThreadExecutor() - anrDetectorExecutorService.executeSafe( + anrDetectorExecutorService?.executeSafe( "ANR detection", sdkCore.internalLogger, - anrDetectorRunnable + detectorRunnable ) + anrDetectorRunnable = detectorRunnable } private fun addJvmCrash(crashEvent: JvmCrash.Rum) { @@ -541,6 +583,7 @@ internal class RumFeature constructor( val telemetryConfigurationMapper: EventMapper, val backgroundEventTracking: Boolean, val trackFrustrations: Boolean, + val trackNonFatalAnrs: Boolean, val vitalsMonitorUpdateFrequency: VitalsUpdateFrequency, val sessionListener: RumSessionListener, val additionalConfig: Map @@ -548,7 +591,6 @@ internal class RumFeature constructor( internal companion object { - internal const val JVM_CRASH_BUS_MESSAGE_TYPE = "jvm_crash" internal const val NDK_CRASH_BUS_MESSAGE_TYPE = "ndk_crash" internal const val LOGGER_ERROR_BUS_MESSAGE_TYPE = "logger_error" internal const val LOGGER_ERROR_WITH_STACK_TRACE_MESSAGE_TYPE = "logger_error_with_stacktrace" @@ -587,6 +629,7 @@ internal class RumFeature constructor( telemetryConfigurationMapper = NoOpEventMapper(), backgroundEventTracking = false, trackFrustrations = true, + trackNonFatalAnrs = isTrackNonFatalAnrsEnabledByDefault(), vitalsMonitorUpdateFrequency = VitalsUpdateFrequency.AVERAGE, sessionListener = NoOpRumSessionListener(), additionalConfig = emptyMap() @@ -605,10 +648,10 @@ internal class RumFeature constructor( "RUM feature receive an event of unsupported type=%s." internal const val UNKNOWN_EVENT_TYPE_PROPERTY_VALUE = "RUM feature received an event with unknown value of \"type\" property=%s." - internal const val JVM_CRASH_EVENT_MISSING_MANDATORY_FIELDS = - "RUM feature received a JVM crash event" + - " where one or more mandatory (throwable, message) fields" + - " are either missing or have a wrong type." + internal const val FAILED_TO_GET_HISTORICAL_EXIT_REASONS = + "Couldn't get historical exit reasons" + internal const val NO_LAST_RUM_VIEW_EVENT_AVAILABLE = + "No last known RUM view event found, skipping fatal ANR reporting." internal const val LOG_ERROR_EVENT_MISSING_MANDATORY_FIELDS = "RUM feature received a log event" + " where mandatory message field is either missing or has a wrong type." @@ -650,5 +693,11 @@ internal class RumFeature constructor( val providers = customProviders + defaultProviders return DatadogGesturesTracker(providers, interactionPredicate, internalLogger) } + + internal fun isTrackNonFatalAnrsEnabledByDefault( + buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT + ): Boolean { + return buildSdkVersionProvider.version < Build.VERSION_CODES.R + } } } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/anr/ANRDetectorRunnable.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/anr/ANRDetectorRunnable.kt index da93791343..bb571a8958 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/anr/ANRDetectorRunnable.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/anr/ANRDetectorRunnable.kt @@ -7,8 +7,13 @@ package com.datadog.android.rum.internal.anr import android.os.Handler -import com.datadog.android.api.SdkCore +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.core.feature.event.ThreadDump +import com.datadog.android.core.internal.utils.asString +import com.datadog.android.core.internal.utils.loggableStackTrace import com.datadog.android.rum.GlobalRumMonitor +import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource /** @@ -17,7 +22,7 @@ import com.datadog.android.rum.RumErrorSource * It runs in a background thread and schedules regular no-op */ internal class ANRDetectorRunnable( - private val sdkCore: SdkCore, + private val sdkCore: FeatureSdkCore, private val handler: Handler, private val anrThresholdMs: Long = ANR_THRESHOLD_MS, private val anrTestDelayMs: Long = ANR_TEST_DELAY_MS @@ -43,11 +48,30 @@ internal class ANRDetectorRunnable( callback.wait(anrThresholdMs) if (!callback.wasCalled()) { + val anrThread = handler.looper.thread + val anrException = ANRException(anrThread) + val allThreads = safeGetAllStacktraces() + .filterValues { it.isNotEmpty() } + .map { + val thread = it.key + val isAnrThread = thread == anrThread + val stack = if (isAnrThread) { + anrException.loggableStackTrace() + } else { + thread.stackTrace.loggableStackTrace() + } + ThreadDump( + name = thread.name, + state = thread.state.asString(), + stack = stack, + crashed = false + ) + } GlobalRumMonitor.get(sdkCore).addError( ANR_MESSAGE, RumErrorSource.SOURCE, - ANRException(handler.looper.thread), - emptyMap() + anrException, + mapOf(RumAttributes.INTERNAL_ALL_THREADS to allThreads) ) callback.wait() } @@ -69,6 +93,20 @@ internal class ANRDetectorRunnable( shouldStop = true } + private fun safeGetAllStacktraces(): Map> { + return try { + Thread.getAllStackTraces() + } catch (e: SecurityException) { + sdkCore.internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { "Failed to get all stack traces." }, + e + ) + emptyMap() + } + } + // We need to let this class extend java's Object @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") internal class CallbackRunnable : Object(), Runnable { diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/anr/AndroidTraceParser.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/anr/AndroidTraceParser.kt new file mode 100644 index 0000000000..dba025d225 --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/anr/AndroidTraceParser.kt @@ -0,0 +1,124 @@ +/* + * 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.anr + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.feature.event.ThreadDump +import java.io.IOException +import java.io.InputStream +import java.util.Locale + +/** + * Thread dump: https://cs.android.com/android/platform/superproject/main/+/main:art/runtime/thread_list.cc;l=255;drc=d00d24530a29b684bec9a895c1da491a6390395f + */ +internal class AndroidTraceParser( + private val internalLogger: InternalLogger +) { + + internal fun parse(traceInputStream: InputStream): List { + @Suppress("UnsafeThirdPartyFunctionCall") // not 3rd party + val trace = traceInputStream.safeReadText() + + if (trace.isBlank()) return emptyList() + + return parse(trace) + } + + @Suppress("CyclomaticComplexMethod") + private fun parse(trace: String): List { + val threadDumps = mutableListOf() + + var isInThreadStackBlock = false + val currentThreadStack = mutableListOf() + var currentThreadName: String? = null + var currentThreadState: String? = null + + @Suppress("LoopWithTooManyJumpStatements") + for (line in trace.lines()) { + // we are leaving thread information block + if (line.isBlank() && isInThreadStackBlock) { + if (currentThreadStack.isNotEmpty() && currentThreadName != null) { + threadDumps += ThreadDump( + name = currentThreadName, + state = convertThreadState(currentThreadState.orEmpty()), + stack = currentThreadStack.joinToString("\n"), + crashed = currentThreadName == "main" + ) + } + currentThreadStack.clear() + isInThreadStackBlock = false + continue + } + // we are entering thread information block + if (line.contains(" prio=") && line.contains(" tid=")) { + isInThreadStackBlock = true + val threadState = line.split(" ") + .lastOrNull() + val threadName = THREAD_NAME_REGEX.matchEntire(line) + ?.groupValues + ?.elementAtOrNull(1) + currentThreadName = threadName + currentThreadState = threadState + continue + } + if (isInThreadStackBlock && line.trimStart() + .let { it.startsWith("at ") || it.startsWith("native: ") } + ) { + // it can be also lines in the stack like: + // - waiting on <0x0dd89f49> (a okhttp3.internal.concurrent.TaskRunner) + // - locked <0x0dd89f49> (a okhttp3.internal.concurrent.TaskRunner) + // we want to skip them for now + // also we want to skip any non-stack lines in the thread info block + currentThreadStack += line + } + } + + if (threadDumps.isEmpty()) { + internalLogger.log( + InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { PARSING_FAILURE_MESSAGE } + ) + } + + return threadDumps + } + + private fun convertThreadState(threadState: String): String { + // https://cs.android.com/android/platform/superproject/main/+/main:art/runtime/thread_state.h;l=30;drc=37cec725ba134174eadf63b0eb06b964ffc202fd + // some values are similar to Java's Thread.State, some are not. For the similar ones we + // need to have a conversion in order to reduce cardinality + val convertedState = when (threadState) { + "TimedWaiting" -> "Timed_Waiting" + else -> threadState + } + return convertedState.lowercase(Locale.US) + } + + private fun InputStream.safeReadText(): String { + return try { + use { + it.reader().readText() + } + } catch (e: IOException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { TRACE_STREAM_READ_FAILURE }, + e + ) + "" + } + } + + companion object { + const val TRACE_STREAM_READ_FAILURE = "Failed to read crash trace stream." + const val PARSING_FAILURE_MESSAGE = + "Parsing tracing information for the exit reason wasn't successful, no thread dumps were parsed." + val THREAD_NAME_REGEX = Regex("^\"(.+)\".+\$") + } +} 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 ca5960cd3c..c9e655eec8 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 @@ -6,15 +6,21 @@ package com.datadog.android.rum.internal.domain.scope +import android.app.ActivityManager import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger import com.datadog.android.api.feature.Feature import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore 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.internal.AppStartTimeProvider +import com.datadog.android.rum.internal.DefaultAppStartTimeProvider import com.datadog.android.rum.internal.domain.RumContext +import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.vitals.VitalMonitor +import java.util.concurrent.TimeUnit @Suppress("LongParameterList") internal class RumApplicationScope( @@ -27,7 +33,8 @@ internal class RumApplicationScope( private val cpuVitalMonitor: VitalMonitor, private val memoryVitalMonitor: VitalMonitor, private val frameRateVitalMonitor: VitalMonitor, - private val sessionListener: RumSessionListener? + private val sessionListener: RumSessionListener?, + private val appStartTimeProvider: AppStartTimeProvider = DefaultAppStartTimeProvider() ) : RumScope, RumViewChangedListener { private var rumContext = RumContext(applicationId = applicationId) @@ -55,6 +62,7 @@ internal class RumApplicationScope( } private var lastActiveViewInfo: RumViewInfo? = null + private var isAppStartedEventSent = false // region RumScope @@ -79,6 +87,10 @@ internal class RumApplicationScope( } } + if (event !is RumRawEvent.SdkInit && !isAppStartedEventSent) { + sendApplicationStartEvent(event.eventTime, writer) + } + delegateToChildren(event, writer) return this @@ -154,10 +166,36 @@ internal class RumApplicationScope( } } + @WorkerThread + private fun sendApplicationStartEvent(eventTime: Time, writer: DataWriter) { + val processImportance = DdRumContentProvider.processImportance + val isForegroundProcess = processImportance == + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + if (isForegroundProcess) { + val processStartTimeNs = appStartTimeProvider.appStartTimeNs + // processStartTime is the time in nanoseconds since VM start. To get a timestamp, we want + // to convert it to milliseconds since epoch provided by System.currentTimeMillis. + // To do so, we take the offset of those times in the event time, which should be consistent, + // then add that to our processStartTime to get the correct value. + val timestampNs = ( + TimeUnit.MILLISECONDS.toNanos(eventTime.timestamp) - eventTime.nanoTime + ) + processStartTimeNs + val applicationLaunchViewTime = Time( + timestamp = TimeUnit.NANOSECONDS.toMillis(timestampNs), + nanoTime = processStartTimeNs + ) + val startupTime = eventTime.nanoTime - processStartTimeNs + val appStartedEvent = + RumRawEvent.ApplicationStarted(applicationLaunchViewTime, startupTime) + delegateToChildren(appStartedEvent, writer) + isAppStartedEventSent = true + } + } + // endregion companion object { internal const val MULTIPLE_ACTIVE_SESSIONS_ERROR = "Application has multiple active " + - "sessions when starting a new session" + "sessions when starting a new session" } } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt index efa1b3b00b..9a52e1816c 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt @@ -220,7 +220,6 @@ internal sealed class RumRawEvent { internal data class SdkInit( val isAppInForeground: Boolean, - val appStartTimeNs: Long, override val eventTime: Time = Time() ) : RumRawEvent() } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt index d778ad887b..1eb717b3bf 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt @@ -140,6 +140,7 @@ internal class RumResourceScope( event.statusCode, event.throwable.loggableStackTrace(), event.throwable.javaClass.canonicalName, + ErrorEvent.Category.EXCEPTION, writer ) } @@ -153,12 +154,16 @@ internal class RumResourceScope( attributes.putAll(event.attributes) + val errorCategory = + if (event.stackTrace.isNotEmpty()) ErrorEvent.Category.EXCEPTION else null + sendError( event.message, event.source, event.statusCode, event.stackTrace, event.errorType, + errorCategory, writer ) } @@ -328,9 +333,11 @@ internal class RumResourceScope( statusCode: Long?, stackTrace: String?, errorType: String?, + errorCategory: ErrorEvent.Category?, writer: DataWriter ) { attributes.putAll(GlobalRumMonitor.get(sdkCore).getAttributes()) + val errorFingerprint = attributes.remove(RumAttributes.ERROR_FINGERPRINT) as? String val rumContext = getRumContext() @@ -364,6 +371,7 @@ internal class RumResourceScope( source = source.toSchemaSource(), stack = stackTrace, isCrash = false, + fingerprint = errorFingerprint, resource = ErrorEvent.Resource( url = url, method = method.toErrorMethod(), @@ -371,6 +379,7 @@ internal class RumResourceScope( provider = resolveErrorProvider() ), type = errorType, + category = errorCategory, sourceType = ErrorEvent.SourceType.ANDROID ), action = rumContext.actionId?.let { ErrorEvent.Action(listOf(it)) }, 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 1b8b2a32fd..22ba5c50e5 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 @@ -13,7 +13,6 @@ 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.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.storage.NoOpDataWriter import com.datadog.android.rum.internal.vitals.VitalMonitor import com.datadog.android.rum.utils.percent @@ -119,19 +118,8 @@ internal class RumSessionScope( val actualWriter = if (sessionState == State.TRACKED) writer else noOpWriter - val downStreamEvent = if (event is RumRawEvent.SdkInit) { - if (event.isAppInForeground) { - createApplicationStartEvent(event) - } else { - // stop here, we initialized the session, no need to go down - null - } - } else { - event - } - - if (downStreamEvent != null) { - childScope = childScope?.handleEvent(downStreamEvent, actualWriter) + if (event !is RumRawEvent.SdkInit) { + childScope = childScope?.handleEvent(event, actualWriter) } return if (isSessionComplete()) { @@ -228,26 +216,6 @@ internal class RumSessionScope( ) } - private fun createApplicationStartEvent( - sdkInitEvent: RumRawEvent.SdkInit - ): RumRawEvent.ApplicationStarted { - val processStartTimeNs = sdkInitEvent.appStartTimeNs - val eventTime = sdkInitEvent.eventTime - // processStartTime is the time in nanoseconds since VM start. To get a timestamp, we want - // to convert it to milliseconds since epoch provided by System.currentTimeMillis. - // To do so, we take the offset of those times in the event time, which should be consistent, - // then add that to our processStartTime to get the correct value. - val timestampNs = ( - TimeUnit.MILLISECONDS.toNanos(eventTime.timestamp) - eventTime.nanoTime - ) + processStartTimeNs - val applicationLaunchViewTime = Time( - timestamp = TimeUnit.NANOSECONDS.toMillis(timestampNs), - nanoTime = processStartTimeNs - ) - val startupTime = sdkInitEvent.eventTime.nanoTime - processStartTimeNs - return RumRawEvent.ApplicationStarted(applicationLaunchViewTime, startupTime) - } - // endregion companion object { 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 cb873293e2..c74cacdb73 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 @@ -279,9 +279,9 @@ internal class RumViewManagerScope( private const val MESSAGE_GAP_BETWEEN_VIEWS = "Gap between views was %d nanoseconds" internal const val MESSAGE_MISSING_VIEW = "A RUM event was detected, but no view is active. " + - "To track views automatically, try calling the " + - "RumConfiguration.Builder.useViewTrackingStrategy() method.\n" + - "You can also track views manually using the RumMonitor.startView() and " + - "RumMonitor.stopView() methods." + "To track views automatically, try calling the " + + "RumConfiguration.Builder.useViewTrackingStrategy() method.\n" + + "You can also track views manually using the RumMonitor.startView() and " + + "RumMonitor.stopView() methods." } } 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 e3510627e2..a7c33dbdcc 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 @@ -20,6 +20,7 @@ import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.RumFeature +import com.datadog.android.rum.internal.anr.ANRException import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.monitor.StorageEvent @@ -59,7 +60,7 @@ internal open class RumViewScope( internal val url = key.url.replace('.', '/') internal val eventAttributes: MutableMap = initialAttributes.toMutableMap() - private var globalAttributes: MutableMap = resolveGlobalAttributes(sdkCore) + private var globalAttributes: Map = resolveGlobalAttributes(sdkCore) private var sessionId: String = parentScope.getRumContext().sessionId internal var viewId: String = UUID.randomUUID().toString() @@ -373,6 +374,7 @@ internal open class RumViewScope( val updatedAttributes = addExtraAttributes(event.attributes) val isFatal = updatedAttributes .remove(RumAttributes.INTERNAL_ERROR_IS_CRASH) as? Boolean == true || event.isFatal + val errorFingerprint = updatedAttributes.remove(RumAttributes.ERROR_FINGERPRINT) as? String // if a cross-platform crash was already reported, do not send its native version if (crashCount > 0 && isFatal) return @@ -383,6 +385,8 @@ internal open class RumViewScope( } else { event.message } + // make a copy - by the time we iterate over it on another thread, it may already be changed + val eventFeatureFlags = featureFlags.toMutableMap() sdkCore.newRumEventWriteOperation(writer) { datadogContext -> @@ -409,14 +413,16 @@ internal open class RumViewScope( } ErrorEvent( date = event.eventTime.timestamp + serverTimeOffsetInMs, - featureFlags = ErrorEvent.Context(featureFlags), + featureFlags = ErrorEvent.Context(eventFeatureFlags), error = ErrorEvent.Error( message = message, source = event.source.toSchemaSource(), stack = event.stacktrace ?: event.throwable?.loggableStackTrace(), isCrash = isFatal, + fingerprint = errorFingerprint, type = errorType, sourceType = event.sourceType.toSchemaSourceType(), + category = ErrorEvent.Category.tryFrom(event), threads = event.threads.map { ErrorEvent.Thread( name = it.name, @@ -856,8 +862,8 @@ internal open class RumViewScope( } } - private fun resolveGlobalAttributes(sdkCore: InternalSdkCore): MutableMap { - return GlobalRumMonitor.get(sdkCore).getAttributes().toMutableMap() + private fun resolveGlobalAttributes(sdkCore: InternalSdkCore): Map { + return GlobalRumMonitor.get(sdkCore).getAttributes().toMap() } private fun resolveViewDuration(event: RumRawEvent): Long { @@ -1124,14 +1130,26 @@ internal open class RumViewScope( private fun isViewComplete(): Boolean { val pending = pendingActionCount + - pendingResourceCount + - pendingErrorCount + - pendingLongTaskCount + pendingResourceCount + + pendingErrorCount + + pendingLongTaskCount // we use <= 0 for pending counter as a safety measure to make sure this ViewScope will // be closed. return stopped && activeResourceScopes.isEmpty() && (pending <= 0L) } + private fun ErrorEvent.Category.Companion.tryFrom( + event: RumRawEvent.AddError + ): ErrorEvent.Category? { + return if (event.throwable != null) { + if (event.throwable is ANRException) ErrorEvent.Category.ANR else ErrorEvent.Category.EXCEPTION + } else if (event.stacktrace != null) { + ErrorEvent.Category.EXCEPTION + } else { + null + } + } + enum class RumViewType(val asString: String) { NONE("NONE"), FOREGROUND("FOREGROUND"), @@ -1151,19 +1169,19 @@ internal open class RumViewScope( internal val ONE_SECOND_NS = TimeUnit.SECONDS.toNanos(1) internal const val ACTION_DROPPED_WARNING = "RUM Action (%s on %s) was dropped, because" + - " another action is still active for the same view" + " another action is still active for the same view" internal const val RUM_CONTEXT_UPDATE_IGNORED_AT_STOP_VIEW_MESSAGE = "Trying to update global RUM context when StopView event arrived, but the context" + - " doesn't reference this view." + " doesn't reference this view." internal const val RUM_CONTEXT_UPDATE_IGNORED_AT_ACTION_UPDATE_MESSAGE = "Trying to update active action in the global RUM context, but the context" + - " doesn't reference this view." + " doesn't reference this view." internal val FROZEN_FRAME_THRESHOLD_NS = TimeUnit.MILLISECONDS.toNanos(700) internal const val SLOW_RENDERED_THRESHOLD_FPS = 55 internal const val NEGATIVE_DURATION_WARNING_MESSAGE = "The computed duration for the " + - "view: %s was 0 or negative. In order to keep the view we forced it to 1ns." + "view: %s was 0 or negative. In order to keep the view we forced it to 1ns." internal fun fromEvent( parentScope: RumScope, 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 a7c7aadba7..53ff7dcf1d 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 @@ -26,9 +26,7 @@ import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum._RumInternalProxy -import com.datadog.android.rum.internal.AppStartTimeProvider import com.datadog.android.rum.internal.CombinedRumSessionListener -import com.datadog.android.rum.internal.DefaultAppStartTimeProvider import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.internal.RumFeature import com.datadog.android.rum.internal.debug.RumDebugListener @@ -71,8 +69,7 @@ internal class DatadogRumMonitor( memoryVitalMonitor: VitalMonitor, frameRateVitalMonitor: VitalMonitor, sessionListener: RumSessionListener, - private val appStartTimeProvider: AppStartTimeProvider = DefaultAppStartTimeProvider(), - private val executorService: ExecutorService = Executors.newSingleThreadExecutor() + internal val executorService: ExecutorService = Executors.newSingleThreadExecutor() ) : RumMonitor, AdvancedRumMonitor { internal var rootScope: RumScope = RumApplicationScope( @@ -194,7 +191,7 @@ internal class DatadogRumMonitor( @Deprecated( "This method is deprecated and will be removed in the future versions." + - " Use `startResource` method which takes `RumHttpMethod` as `method` parameter instead." + " Use `startResource` method which takes `RumHttpMethod` as `method` parameter instead." ) override fun startResource( key: String, @@ -312,6 +309,10 @@ internal class DatadogRumMonitor( ) { val eventTime = getEventTime(attributes) val errorType = getErrorType(attributes) + val mutableAttributes = attributes.toMutableMap() + + @Suppress("UNCHECKED_CAST") + val threads = mutableAttributes.remove(RumAttributes.INTERNAL_ALL_THREADS) as? List handleEvent( RumRawEvent.AddError( message, @@ -319,10 +320,10 @@ internal class DatadogRumMonitor( throwable, null, false, - attributes.toMap(), + mutableAttributes, eventTime, errorType, - threads = emptyList() + threads = threads.orEmpty() ) ) } @@ -408,10 +409,9 @@ internal class DatadogRumMonitor( override fun start() { val processImportance = DdRumContentProvider.processImportance val isAppInForeground = processImportance == - ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND - val processStartTimeNs = appStartTimeProvider.appStartTimeNs + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND handleEvent( - RumRawEvent.SdkInit(isAppInForeground, processStartTimeNs) + RumRawEvent.SdkInit(isAppInForeground) ) } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/ndk/NdkCrashEventHandler.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/ndk/NdkCrashEventHandler.kt deleted file mode 100644 index e0a0c1b1d2..0000000000 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/ndk/NdkCrashEventHandler.kt +++ /dev/null @@ -1,14 +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.ndk - -import com.datadog.android.api.feature.FeatureSdkCore -import com.datadog.android.api.storage.DataWriter - -internal interface NdkCrashEventHandler { - fun handleEvent(event: Map<*, *>, sdkCore: FeatureSdkCore, rumWriter: DataWriter) -} diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacks.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacks.kt index 15003cc256..c6107e26e8 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacks.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacks.kt @@ -13,7 +13,6 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.SdkCore import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.core.internal.system.BuildSdkVersionProvider -import com.datadog.android.core.internal.system.DefaultBuildSdkVersionProvider import com.datadog.android.core.internal.thread.LoggingScheduledThreadPoolExecutor import com.datadog.android.core.internal.utils.scheduleSafe import com.datadog.android.rum.RumMonitor @@ -31,7 +30,7 @@ internal class OreoFragmentLifecycleCallbacks( private val componentPredicate: ComponentPredicate, private val rumFeature: RumFeature, private val rumMonitor: RumMonitor, - private val buildSdkVersionProvider: BuildSdkVersionProvider = DefaultBuildSdkVersionProvider() + private val buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT ) : FragmentLifecycleCallbacks, FragmentManager.FragmentLifecycleCallbacks() { private lateinit var sdkCore: FeatureSdkCore @@ -50,13 +49,13 @@ internal class OreoFragmentLifecycleCallbacks( override fun register(activity: Activity, sdkCore: SdkCore) { this.sdkCore = sdkCore as FeatureSdkCore - if (buildSdkVersionProvider.version() >= Build.VERSION_CODES.O) { + if (buildSdkVersionProvider.version >= Build.VERSION_CODES.O) { activity.fragmentManager.registerFragmentLifecycleCallbacks(this, true) } } override fun unregister(activity: Activity) { - if (buildSdkVersionProvider.version() >= Build.VERSION_CODES.O) { + if (buildSdkVersionProvider.version >= Build.VERSION_CODES.O) { activity.fragmentManager.unregisterFragmentLifecycleCallbacks(this) } } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/vitals/JankStatsActivityLifecycleListener.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/vitals/JankStatsActivityLifecycleListener.kt index adef38e782..22f9889f6f 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/vitals/JankStatsActivityLifecycleListener.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/vitals/JankStatsActivityLifecycleListener.kt @@ -24,7 +24,6 @@ import androidx.metrics.performance.FrameData import androidx.metrics.performance.JankStats import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.system.BuildSdkVersionProvider -import com.datadog.android.core.internal.system.DefaultBuildSdkVersionProvider import java.lang.ref.WeakReference import java.util.WeakHashMap import java.util.concurrent.TimeUnit @@ -37,8 +36,7 @@ internal class JankStatsActivityLifecycleListener( private val internalLogger: InternalLogger, private val jankStatsProvider: JankStatsProvider = JankStatsProvider.DEFAULT, private var screenRefreshRate: Double = 60.0, - private var buildSdkVersionProvider: BuildSdkVersionProvider = DefaultBuildSdkVersionProvider() - + private var buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT ) : ActivityLifecycleCallbacks, JankStats.OnFrameListener { internal val activeWindowsListener = WeakHashMap() @@ -132,7 +130,7 @@ internal class JankStatsActivityLifecycleListener( if (activeActivities[activity.window].isNullOrEmpty()) { activeWindowsListener.remove(activity.window) activeActivities.remove(activity.window) - if (buildSdkVersionProvider.version() >= Build.VERSION_CODES.S) { + if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) { unregisterMetricListener(activity.window) } } @@ -147,9 +145,9 @@ internal class JankStatsActivityLifecycleListener( if (durationNs > 0.0) { var frameRate = (ONE_SECOND_NS / durationNs) - if (buildSdkVersionProvider.version() >= Build.VERSION_CODES.S) { + if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) { screenRefreshRate = ONE_SECOND_NS / frameDeadline - } else if (buildSdkVersionProvider.version() == Build.VERSION_CODES.R) { + } else if (buildSdkVersionProvider.version == Build.VERSION_CODES.R) { screenRefreshRate = display?.refreshRate?.toDouble() ?: SIXTY_FPS } @@ -205,9 +203,9 @@ internal class JankStatsActivityLifecycleListener( @SuppressLint("NewApi") @MainThread private fun trackWindowMetrics(isKnownWindow: Boolean, window: Window, activity: Activity) { - if (buildSdkVersionProvider.version() >= Build.VERSION_CODES.S && !isKnownWindow) { + if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S && !isKnownWindow) { registerMetricListener(window) - } else if (display == null && buildSdkVersionProvider.version() == Build.VERSION_CODES.R) { + } else if (display == null && buildSdkVersionProvider.version == Build.VERSION_CODES.R) { // Fallback - Android 30 allows apps to not run at a fixed 60hz, but didn't yet have // Frame Metrics callbacks available val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager @@ -278,7 +276,7 @@ internal class JankStatsActivityLifecycleListener( internal const val JANK_STATS_TRACKING_ALREADY_DISABLED_ERROR = "Trying to disable JankStats instance which was already disabled before, this" + - " shouldn't happen." + " shouldn't happen." internal const val JANK_STATS_TRACKING_DISABLE_ERROR = "Failed to disable JankStats tracking" diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategy.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategy.kt index 0c6b156f49..303aeecbc2 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategy.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategy.kt @@ -6,12 +6,14 @@ package com.datadog.android.rum.tracking +import android.annotation.SuppressLint import android.app.Activity import android.os.Build import androidx.annotation.MainThread import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import com.datadog.android.api.feature.Feature +import com.datadog.android.core.internal.system.BuildSdkVersionProvider import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.internal.RumFeature import com.datadog.android.rum.internal.tracking.AndroidXFragmentLifecycleCallbacks @@ -26,25 +28,42 @@ import com.datadog.android.rum.internal.tracking.OreoFragmentLifecycleCallbacks * * **Note**: This version of the [FragmentViewTrackingStrategy] is compatible with * the AndroidX Compat Library. - * @param trackArguments whether we track Fragment arguments - * @param supportFragmentComponentPredicate to accept the Androidx Fragments - * that will be taken into account as valid RUM View events. - * @param defaultFragmentComponentPredicate to accept the default Android Fragments - * that will be taken into account as valid RUM View events. */ @Suppress("DEPRECATION") +@SuppressLint("NewApi") class FragmentViewTrackingStrategy -@JvmOverloads -constructor( +internal constructor( internal val trackArguments: Boolean, - internal val supportFragmentComponentPredicate: ComponentPredicate = - AcceptAllSupportFragments(), - internal val defaultFragmentComponentPredicate: ComponentPredicate = - AcceptAllDefaultFragment() + internal val supportFragmentComponentPredicate: ComponentPredicate, + internal val defaultFragmentComponentPredicate: ComponentPredicate, + internal val buildSdkVersionProvider: BuildSdkVersionProvider ) : ActivityLifecycleTrackingStrategy(), ViewTrackingStrategy { + /** + * Creates instance of [FragmentViewTrackingStrategy]. + * + * @param trackArguments whether we track Fragment arguments + * @param supportFragmentComponentPredicate to accept the Androidx Fragments + * that will be taken into account as valid RUM View events. + * @param defaultFragmentComponentPredicate to accept the default Android Fragments + * that will be taken into account as valid RUM View events. + */ + @JvmOverloads + constructor( + trackArguments: Boolean, + supportFragmentComponentPredicate: ComponentPredicate = + AcceptAllSupportFragments(), + defaultFragmentComponentPredicate: ComponentPredicate = + AcceptAllDefaultFragment() + ) : this( + trackArguments, + supportFragmentComponentPredicate, + defaultFragmentComponentPredicate, + BuildSdkVersionProvider.DEFAULT + ) + private val androidXLifecycleCallbacks: FragmentLifecycleCallbacks by lazy { val rumFeature = withSdkCore { @@ -64,6 +83,7 @@ constructor( NoOpFragmentLifecycleCallbacks() } } + private val oreoLifecycleCallbacks: FragmentLifecycleCallbacks by lazy { val rumFeature = withSdkCore { @@ -71,7 +91,7 @@ constructor( } val rumMonitor = withSdkCore { GlobalRumMonitor.get(it) } if ( - Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + buildSdkVersionProvider.version >= Build.VERSION_CODES.O && rumFeature != null && rumMonitor != null ) { OreoFragmentLifecycleCallbacks( @@ -80,7 +100,8 @@ constructor( }, componentPredicate = defaultFragmentComponentPredicate, rumMonitor = rumMonitor, - rumFeature = rumFeature + rumFeature = rumFeature, + buildSdkVersionProvider = buildSdkVersionProvider ) } else { NoOpFragmentLifecycleCallbacks() diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumConfigurationBuilderTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumConfigurationBuilderTest.kt index 3070130691..d20b843e86 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumConfigurationBuilderTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumConfigurationBuilderTest.kt @@ -92,6 +92,11 @@ internal class RumConfigurationBuilderTest { longTaskTrackingStrategy = MainLooperLongTaskStrategy(100L), backgroundEventTracking = false, trackFrustrations = true, + // on Android R+ this should be false, but since default value is static property + // RumFeature.DEFAULT_RUM_CONFIG, it is evaluated at the static() block during class + // loading, so we are not able to set Build API version at this point. We will test + // it through a helper method in RumFeature.Companion + trackNonFatalAnrs = true, vitalsMonitorUpdateFrequency = VitalsUpdateFrequency.AVERAGE, sessionListener = NoOpRumSessionListener(), additionalConfig = emptyMap() @@ -319,6 +324,23 @@ internal class RumConfigurationBuilderTest { ) } + @Test + fun `𝕄 build config with track non-fatal ANRs 𝕎 trackNonFatalAnrs() and build()`( + @BoolForgery trackNonFatalAnrsEnabled: Boolean + ) { + // When + val rumConfiguration = testedBuilder + .trackNonFatalAnrs(trackNonFatalAnrsEnabled) + .build() + + // Then + assertThat(rumConfiguration.featureConfiguration).isEqualTo( + RumFeature.DEFAULT_RUM_CONFIG.copy( + trackNonFatalAnrs = trackNonFatalAnrsEnabled + ) + ) + } + @Test fun `𝕄 build config with RUM View eventMapper 𝕎 setViewEventMapper() and build()`() { // Given diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ErrorEventAssert.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ErrorEventAssert.kt index 83c44b7c70..8336e99439 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ErrorEventAssert.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ErrorEventAssert.kt @@ -69,6 +69,16 @@ internal class ErrorEventAssert(actual: ErrorEvent) : return this } + fun hasErrorFingerprint(expected: String?): ErrorEventAssert { + assertThat(actual.error.fingerprint) + .overridingErrorMessage( + "Expected event data to have error.fingerprint $expected " + + "but was ${actual.error.fingerprint}" + ) + .isEqualTo(expected) + return this + } + fun isCrash(expected: Boolean): ErrorEventAssert { assertThat(actual.error.isCrash) .overridingErrorMessage( @@ -555,7 +565,7 @@ internal class ErrorEventAssert(actual: ErrorEvent) : assertThat(actual.error.threads) .overridingErrorMessage( "Expected RUM event to have error.threads: $expected" + - " but instead was: ${actual.error.threads}" + " but instead was: ${actual.error.threads}" ) .isEqualTo(expected) return this @@ -565,12 +575,22 @@ internal class ErrorEventAssert(actual: ErrorEvent) : assertThat(actual.error.threads) .overridingErrorMessage( "Expected RUM event to not have error.threads," + - " but instead was: ${actual.error.threads}" + " but instead was: ${actual.error.threads}" ) .isNull() return this } + fun hasErrorCategory(category: ErrorEvent.Category?): ErrorEventAssert { + assertThat(actual.error.category) + .overridingErrorMessage( + "Expected RUM event to have error.category: $category" + + " but instead was: ${actual.error.category}" + ) + .isEqualTo(category) + return this + } + companion object { internal const val TIMESTAMP_THRESHOLD_MS = 50L internal fun assertThat(actual: ErrorEvent): ErrorEventAssert = diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/DatadogLateCrashReporterTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/DatadogLateCrashReporterTest.kt new file mode 100644 index 0000000000..66d18d3739 --- /dev/null +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/DatadogLateCrashReporterTest.kt @@ -0,0 +1,1130 @@ +/* + * 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 + +import android.app.ApplicationExitInfo +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.context.UserInfo +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.storage.DataWriter +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.feature.event.ThreadDump +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.rum.RumErrorSource +import com.datadog.android.rum.assertj.ErrorEventAssert +import com.datadog.android.rum.assertj.ViewEventAssert +import com.datadog.android.rum.internal.anr.ANRDetectorRunnable +import com.datadog.android.rum.internal.anr.ANRException +import com.datadog.android.rum.internal.anr.AndroidTraceParser +import com.datadog.android.rum.internal.domain.RumContext +import com.datadog.android.rum.internal.domain.scope.toErrorSchemaType +import com.datadog.android.rum.model.ErrorEvent +import com.datadog.android.rum.model.ViewEvent +import com.datadog.android.rum.utils.forge.Configurator +import com.datadog.android.rum.utils.verifyLog +import com.google.gson.JsonObject +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +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.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.mockito.Mock +import org.mockito.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.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DatadogLateCrashReporterTest { + + private lateinit var testedHandler: LateCrashReporter + + @Mock + lateinit var mockSdkCore: InternalSdkCore + + @Mock + lateinit var mockRumFeatureScope: FeatureScope + + @Mock + lateinit var mockRumWriter: DataWriter + + @Mock + lateinit var mockRumEventDeserializer: Deserializer + + @Mock + lateinit var mockEventBatchWriter: EventBatchWriter + + @Mock + lateinit var mockAndroidTraceParser: AndroidTraceParser + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Forgery + lateinit var fakeDatadogContext: DatadogContext + + @BeforeEach + fun `set up`() { + whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mockRumFeatureScope + + whenever(mockRumFeatureScope.withWriteContext(any(), any())) doAnswer { + val callback = it.getArgument<(DatadogContext, EventBatchWriter) -> Unit>(1) + callback.invoke(fakeDatadogContext, mockEventBatchWriter) + } + + testedHandler = DatadogLateCrashReporter( + sdkCore = mockSdkCore, + rumEventDeserializer = mockRumEventDeserializer, + androidTraceParser = mockAndroidTraceParser + ) + } + + // region handleNdkCrashEvent + + @Test + fun `𝕄 send RUM view+error 𝕎 handleNdkCrashEvent()`( + @StringForgery crashMessage: String, + @LongForgery(min = 1) fakeTimestamp: Long, + @StringForgery fakeSignalName: String, + @StringForgery fakeStacktrace: String, + @Forgery viewEvent: ViewEvent, + @Forgery fakeUserInfo: UserInfo, + forge: Forge + ) { + // Given + val fakeServerOffset = + forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerOffset + ) + ) + + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = 0L, + max = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 + ), + usr = ViewEvent.Usr( + id = fakeUserInfo.id, + name = fakeUserInfo.name, + email = fakeUserInfo.email, + additionalProperties = fakeUserInfo.additionalProperties.toMutableMap() + ) + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) + .doReturn(fakeViewEvent) + + val fakeEvent = mapOf( + "timestamp" to fakeTimestamp, + "signalName" to fakeSignalName, + "stacktrace" to fakeStacktrace, + "message" to crashMessage, + "lastViewEvent" to fakeViewEventJson + ) + + // When + testedHandler.handleNdkCrashEvent(fakeEvent, mockRumWriter) + + // Then + argumentCaptor { + verify(mockRumWriter, times(2)).write(eq(mockEventBatchWriter), capture()) + + ErrorEventAssert.assertThat(firstValue as ErrorEvent) + .hasApplicationId(fakeViewEvent.application.id) + .hasSessionId(fakeViewEvent.session.id) + .hasView( + fakeViewEvent.view.id, + fakeViewEvent.view.name, + fakeViewEvent.view.url + ) + .hasMessage(crashMessage) + .hasStackTrace(fakeStacktrace) + .isCrash(true) + .hasErrorSource(RumErrorSource.SOURCE) + .hasErrorSourceType(ErrorEvent.SourceType.NDK) + .hasTimestamp(fakeTimestamp + fakeServerOffset) + .hasUserInfo( + UserInfo( + fakeViewEvent.usr?.id, + fakeViewEvent.usr?.name, + fakeViewEvent.usr?.email, + fakeViewEvent.usr?.additionalProperties.orEmpty() + ) + ) + .hasErrorType(fakeSignalName) + .hasErrorCategory(ErrorEvent.Category.EXCEPTION) + .hasLiteSessionPlan() + .hasDeviceInfo( + fakeDatadogContext.deviceInfo.deviceName, + fakeDatadogContext.deviceInfo.deviceModel, + fakeDatadogContext.deviceInfo.deviceBrand, + fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), + fakeDatadogContext.deviceInfo.architecture + ) + .hasOsInfo( + fakeDatadogContext.deviceInfo.osName, + fakeDatadogContext.deviceInfo.osVersion, + fakeDatadogContext.deviceInfo.osMajorVersion + ) + + ViewEventAssert.assertThat(secondValue as ViewEvent) + .hasVersion(fakeViewEvent.dd.documentVersion + 1) + .hasCrashCount((fakeViewEvent.view.crash?.count ?: 0) + 1) + .isActive(false) + } + } + + @Test + fun `𝕄 send RUM view+error 𝕎 handleNdkCrashEvent() {source_type set}`( + @StringForgery crashMessage: String, + @LongForgery(min = 1) fakeTimestamp: Long, + @StringForgery fakeSignalName: String, + @StringForgery fakeStacktrace: String, + @Forgery viewEvent: ViewEvent, + @Forgery fakeUserInfo: UserInfo, + forge: Forge + ) { + // Given + val fakeServerOffset = + forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerOffset + ) + ) + + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = 0L, + max = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 + ), + usr = ViewEvent.Usr( + id = fakeUserInfo.id, + name = fakeUserInfo.name, + email = fakeUserInfo.email, + additionalProperties = fakeUserInfo.additionalProperties.toMutableMap() + ) + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) + .doReturn(fakeViewEvent) + + val fakeEvent = mapOf( + "sourceType" to "ndk+il2cpp", + "timestamp" to fakeTimestamp, + "signalName" to fakeSignalName, + "stacktrace" to fakeStacktrace, + "message" to crashMessage, + "lastViewEvent" to fakeViewEventJson + ) + + // When + testedHandler.handleNdkCrashEvent(fakeEvent, mockRumWriter) + + // Then + argumentCaptor { + verify(mockRumWriter, times(2)).write(eq(mockEventBatchWriter), capture()) + + ErrorEventAssert.assertThat(firstValue as ErrorEvent) + .hasApplicationId(fakeViewEvent.application.id) + .hasSessionId(fakeViewEvent.session.id) + .hasView( + fakeViewEvent.view.id, + fakeViewEvent.view.name, + fakeViewEvent.view.url + ) + .hasMessage(crashMessage) + .hasStackTrace(fakeStacktrace) + .isCrash(true) + .hasErrorSource(RumErrorSource.SOURCE) + .hasErrorSourceType(ErrorEvent.SourceType.NDK_IL2CPP) + .hasTimestamp(fakeTimestamp + fakeServerOffset) + .hasUserInfo( + UserInfo( + fakeViewEvent.usr?.id, + fakeViewEvent.usr?.name, + fakeViewEvent.usr?.email, + fakeViewEvent.usr?.additionalProperties.orEmpty() + ) + ) + .hasErrorType(fakeSignalName) + .hasErrorCategory(ErrorEvent.Category.EXCEPTION) + .hasLiteSessionPlan() + .hasDeviceInfo( + fakeDatadogContext.deviceInfo.deviceName, + fakeDatadogContext.deviceInfo.deviceModel, + fakeDatadogContext.deviceInfo.deviceBrand, + fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), + fakeDatadogContext.deviceInfo.architecture + ) + .hasOsInfo( + fakeDatadogContext.deviceInfo.osName, + fakeDatadogContext.deviceInfo.osVersion, + fakeDatadogContext.deviceInfo.osMajorVersion + ) + + ViewEventAssert.assertThat(secondValue as ViewEvent) + .hasVersion(fakeViewEvent.dd.documentVersion + 1) + .hasCrashCount((fakeViewEvent.view.crash?.count ?: 0) + 1) + .isActive(false) + } + } + + @Test + fun `𝕄 send RUM view+error 𝕎 handleNdkCrashEvent() {invalid source_type set}`( + @StringForgery crashMessage: String, + @LongForgery(min = 1) fakeTimestamp: Long, + @StringForgery fakeSignalName: String, + @StringForgery fakeStacktrace: String, + @Forgery viewEvent: ViewEvent, + @Forgery fakeUserInfo: UserInfo, + forge: Forge + ) { + // Given + val fakeServerOffset = + forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerOffset + ) + ) + + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = 0L, + max = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 + ), + usr = ViewEvent.Usr( + id = fakeUserInfo.id, + name = fakeUserInfo.name, + email = fakeUserInfo.email, + additionalProperties = fakeUserInfo.additionalProperties.toMutableMap() + ) + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) + .doReturn(fakeViewEvent) + + val fakeEvent = mapOf( + "sourceType" to "invalid", + "timestamp" to fakeTimestamp, + "signalName" to fakeSignalName, + "stacktrace" to fakeStacktrace, + "message" to crashMessage, + "lastViewEvent" to fakeViewEventJson + ) + + // When + testedHandler.handleNdkCrashEvent(fakeEvent, mockRumWriter) + + // Then + argumentCaptor { + verify(mockRumWriter, times(2)).write(eq(mockEventBatchWriter), capture()) + + ErrorEventAssert.assertThat(firstValue as ErrorEvent) + .hasErrorSourceType(ErrorEvent.SourceType.NDK) + } + } + + @Test + fun `𝕄 send RUM view+error 𝕎 handleNdkCrashEvent() {view without usr}`( + @StringForgery crashMessage: String, + @LongForgery(min = 1) fakeTimestamp: Long, + @StringForgery fakeSignalName: String, + @StringForgery fakeStacktrace: String, + @Forgery viewEvent: ViewEvent, + forge: Forge + ) { + // Given + val fakeServerOffset = + forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerOffset + ) + ) + + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = 0L, + max = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 + ), + usr = null + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + val fakeEvent = mapOf( + "timestamp" to fakeTimestamp, + "signalName" to fakeSignalName, + "stacktrace" to fakeStacktrace, + "message" to crashMessage, + "lastViewEvent" to fakeViewEventJson + ) + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) + .doReturn(fakeViewEvent) + + // When + testedHandler.handleNdkCrashEvent(fakeEvent, mockRumWriter) + + // Then + argumentCaptor { + verify(mockRumWriter, times(2)).write(eq(mockEventBatchWriter), capture()) + + ErrorEventAssert.assertThat(firstValue as ErrorEvent) + .hasApplicationId(fakeViewEvent.application.id) + .hasSessionId(fakeViewEvent.session.id) + .hasView( + fakeViewEvent.view.id, + fakeViewEvent.view.name, + fakeViewEvent.view.url + ) + .hasMessage(crashMessage) + .hasStackTrace(fakeStacktrace) + .isCrash(true) + .hasErrorSource(RumErrorSource.SOURCE) + .hasErrorSourceType(ErrorEvent.SourceType.NDK) + .hasTimestamp(fakeTimestamp + fakeServerOffset) + .hasNoUserInfo() + .hasErrorType(fakeSignalName) + .hasErrorCategory(ErrorEvent.Category.EXCEPTION) + .hasLiteSessionPlan() + .hasDeviceInfo( + fakeDatadogContext.deviceInfo.deviceName, + fakeDatadogContext.deviceInfo.deviceModel, + fakeDatadogContext.deviceInfo.deviceBrand, + fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), + fakeDatadogContext.deviceInfo.architecture + ) + .hasOsInfo( + fakeDatadogContext.deviceInfo.osName, + fakeDatadogContext.deviceInfo.osVersion, + fakeDatadogContext.deviceInfo.osMajorVersion + ) + + ViewEventAssert.assertThat(secondValue as ViewEvent) + .hasVersion(fakeViewEvent.dd.documentVersion + 1) + .hasCrashCount((fakeViewEvent.view.crash?.count ?: 0) + 1) + .isActive(false) + } + } + + @Test + fun `𝕄 send only RUM error 𝕎 handleNdkCrashEvent() {view is too old}`( + @StringForgery crashMessage: String, + @LongForgery(min = 1) fakeTimestamp: Long, + @StringForgery fakeSignalName: String, + @StringForgery fakeStacktrace: String, + @Forgery viewEvent: ViewEvent, + @Forgery fakeUserInfo: UserInfo, + forge: Forge + ) { + // Given + val fakeServerOffset = + forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerOffset + ) + ) + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD + 1 + ), + usr = ViewEvent.Usr( + id = fakeUserInfo.id, + name = fakeUserInfo.name, + email = fakeUserInfo.email, + additionalProperties = fakeUserInfo.additionalProperties.toMutableMap() + ) + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) + .doReturn(fakeViewEvent) + val expectedErrorEventSource = with(fakeViewEvent.source) { + if (this != null) { + ErrorEvent.ErrorEventSource.fromJson(this.toJson().asString) + } else { + null + } + } + val fakeEvent = mapOf( + "timestamp" to fakeTimestamp, + "signalName" to fakeSignalName, + "stacktrace" to fakeStacktrace, + "message" to crashMessage, + "lastViewEvent" to fakeViewEventJson + ) + + // When + testedHandler.handleNdkCrashEvent(fakeEvent, mockRumWriter) + + // Then + argumentCaptor { + verify(mockRumWriter, times(1)).write(eq(mockEventBatchWriter), capture()) + + ErrorEventAssert.assertThat(firstValue as ErrorEvent) + .hasApplicationId(fakeViewEvent.application.id) + .hasSessionId(fakeViewEvent.session.id) + .hasView( + fakeViewEvent.view.id, + fakeViewEvent.view.name, + fakeViewEvent.view.url + ) + .hasMessage(crashMessage) + .hasStackTrace(fakeStacktrace) + .isCrash(true) + .hasErrorSource(RumErrorSource.SOURCE) + .hasErrorSourceType(ErrorEvent.SourceType.NDK) + .hasTimestamp(fakeTimestamp + fakeServerOffset) + .hasUserInfo( + UserInfo( + fakeViewEvent.usr?.id, + fakeViewEvent.usr?.name, + fakeViewEvent.usr?.email, + fakeViewEvent.usr?.additionalProperties.orEmpty() + ) + ) + .hasErrorType(fakeSignalName) + .hasErrorCategory(ErrorEvent.Category.EXCEPTION) + .hasLiteSessionPlan() + .hasSource(expectedErrorEventSource) + .hasDeviceInfo( + fakeDatadogContext.deviceInfo.deviceName, + fakeDatadogContext.deviceInfo.deviceModel, + fakeDatadogContext.deviceInfo.deviceBrand, + fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), + fakeDatadogContext.deviceInfo.architecture + ) + .hasOsInfo( + fakeDatadogContext.deviceInfo.osName, + fakeDatadogContext.deviceInfo.osVersion, + fakeDatadogContext.deviceInfo.osMajorVersion + ) + } + } + + @Test + fun `𝕄 not send RUM event 𝕎 handleNdkCrashEvent() { RUM feature is not registered }`( + @StringForgery crashMessage: String, + @LongForgery(min = 1) fakeTimestamp: Long, + @StringForgery fakeSignalName: String, + @StringForgery fakeStacktrace: String, + @Forgery viewEvent: ViewEvent + ) { + // Given + val fakeViewEventJson = viewEvent.toJson().asJsonObject + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null + val fakeEvent = mapOf( + "timestamp" to fakeTimestamp, + "signalName" to fakeSignalName, + "stacktrace" to fakeStacktrace, + "message" to crashMessage, + "lastViewEvent" to fakeViewEventJson + ) + + // When + testedHandler.handleNdkCrashEvent(fakeEvent, mockRumWriter) + + // Then + verifyNoInteractions(mockRumWriter, mockEventBatchWriter) + mockInternalLogger.verifyLog( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + DatadogLateCrashReporter.INFO_RUM_FEATURE_NOT_REGISTERED + ) + } + + @Test + fun `𝕄 not send RUM event 𝕎 handleNdkCrashEvent() { corrupted event, view json deserialization fails }`( + @StringForgery crashMessage: String, + @LongForgery(min = 1) fakeTimestamp: Long, + @StringForgery fakeSignalName: String, + @StringForgery fakeStacktrace: String, + @Forgery fakeViewEventJson: JsonObject + ) { + // Given + val fakeEvent = mutableMapOf( + "timestamp" to fakeTimestamp, + "signalName" to fakeSignalName, + "stacktrace" to fakeStacktrace, + "message" to crashMessage, + "lastViewEvent" to fakeViewEventJson + ) + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) doReturn null + + // When + testedHandler.handleNdkCrashEvent(fakeEvent, mockRumWriter) + + // Then + verifyNoInteractions(mockRumWriter, mockEventBatchWriter) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + DatadogLateCrashReporter.NDK_CRASH_EVENT_MISSING_MANDATORY_FIELDS + ) + } + + @ParameterizedTest + @EnumSource(ValueMissingType::class) + fun `𝕄 not send RUM event 𝕎 handleNdkCrashEvent() { corrupted event }`( + missingType: ValueMissingType, + @StringForgery crashMessage: String, + @LongForgery(min = 1) fakeTimestamp: Long, + @StringForgery fakeSignalName: String, + @StringForgery fakeStacktrace: String, + @Forgery viewEvent: ViewEvent, + forge: Forge + ) { + // Given + val fakeViewEventJson = viewEvent.toJson().asJsonObject + val fakeEvent = mutableMapOf( + "timestamp" to fakeTimestamp, + "signalName" to fakeSignalName, + "stacktrace" to fakeStacktrace, + "message" to crashMessage, + "lastViewEvent" to fakeViewEventJson + ) + + when (missingType) { + ValueMissingType.MISSING -> fakeEvent.remove(forge.anElementFrom(fakeEvent.keys)) + ValueMissingType.NULL -> fakeEvent[forge.anElementFrom(fakeEvent.keys)] = null + ValueMissingType.WRONG_TYPE -> fakeEvent[forge.anElementFrom(fakeEvent.keys)] = Any() + } + + // When + testedHandler.handleNdkCrashEvent(fakeEvent, mockRumWriter) + + // Then + verifyNoInteractions(mockRumWriter, mockEventBatchWriter) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + DatadogLateCrashReporter.NDK_CRASH_EVENT_MISSING_MANDATORY_FIELDS + ) + } + + //endregion + + // region handleAnrCrash + + @Test + fun `𝕄 send RUM view+error 𝕎 handleAnrCrash()`( + @LongForgery(min = 1) fakeTimestamp: Long, + @Forgery viewEvent: ViewEvent, + @Forgery fakeUserInfo: UserInfo, + forge: Forge + ) { + // Given + val fakeServerOffset = + forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerOffset + ) + ) + + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = 0L, + max = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 + ), + usr = ViewEvent.Usr( + id = fakeUserInfo.id, + name = fakeUserInfo.name, + email = fakeUserInfo.email, + additionalProperties = fakeUserInfo.additionalProperties.toMutableMap() + ) + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) doReturn fakeViewEvent + + val fakeThreadsDump = forge.anrCrashThreadDump() + whenever(mockAndroidTraceParser.parse(any())) doReturn fakeThreadsDump + val mockAnrExitInfo = mock().apply { + whenever(traceInputStream) doReturn mock() + whenever(timestamp) doReturn fakeTimestamp + } + + // When + testedHandler.handleAnrCrash(mockAnrExitInfo, fakeViewEventJson, mockRumWriter) + + // Then + argumentCaptor { + verify(mockRumWriter, times(2)).write(eq(mockEventBatchWriter), capture()) + + ErrorEventAssert.assertThat(firstValue as ErrorEvent) + .hasApplicationId(fakeViewEvent.application.id) + .hasSessionId(fakeViewEvent.session.id) + .hasView( + fakeViewEvent.view.id, + fakeViewEvent.view.name, + fakeViewEvent.view.url + ) + .hasMessage(ANRDetectorRunnable.ANR_MESSAGE) + .hasStackTrace(fakeThreadsDump.first { it.name == "main" }.stack) + .isCrash(true) + .hasErrorSource(RumErrorSource.SOURCE) + .hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + .hasTimestamp(fakeTimestamp + fakeServerOffset) + .hasUserInfo( + UserInfo( + fakeViewEvent.usr?.id, + fakeViewEvent.usr?.name, + fakeViewEvent.usr?.email, + fakeViewEvent.usr?.additionalProperties.orEmpty() + ) + ) + .hasErrorType(ANRException::class.java.canonicalName) + .hasErrorCategory(ErrorEvent.Category.ANR) + .hasLiteSessionPlan() + .hasDeviceInfo( + fakeDatadogContext.deviceInfo.deviceName, + fakeDatadogContext.deviceInfo.deviceModel, + fakeDatadogContext.deviceInfo.deviceBrand, + fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), + fakeDatadogContext.deviceInfo.architecture + ) + .hasOsInfo( + fakeDatadogContext.deviceInfo.osName, + fakeDatadogContext.deviceInfo.osVersion, + fakeDatadogContext.deviceInfo.osMajorVersion + ) + .hasThreads(fakeThreadsDump) + + ViewEventAssert.assertThat(secondValue as ViewEvent) + .hasVersion(fakeViewEvent.dd.documentVersion + 1) + .hasCrashCount((fakeViewEvent.view.crash?.count ?: 0) + 1) + .isActive(false) + } + + verify(mockSdkCore).writeLastFatalAnrSent(fakeTimestamp) + } + + @Test + fun `𝕄 send RUM view+error 𝕎 handleAnrCrash() { view without user }`( + @LongForgery(min = 1) fakeTimestamp: Long, + @Forgery viewEvent: ViewEvent, + forge: Forge + ) { + // Given + val fakeServerOffset = + forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerOffset + ) + ) + + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = 0L, + max = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 + ), + usr = null + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) doReturn fakeViewEvent + + val fakeThreadsDump = forge.anrCrashThreadDump() + whenever(mockAndroidTraceParser.parse(any())) doReturn fakeThreadsDump + val mockAnrExitInfo = mock().apply { + whenever(traceInputStream) doReturn mock() + whenever(timestamp) doReturn fakeTimestamp + } + + // When + testedHandler.handleAnrCrash(mockAnrExitInfo, fakeViewEventJson, mockRumWriter) + + // Then + argumentCaptor { + verify(mockRumWriter, times(2)).write(eq(mockEventBatchWriter), capture()) + + ErrorEventAssert.assertThat(firstValue as ErrorEvent) + .hasApplicationId(fakeViewEvent.application.id) + .hasSessionId(fakeViewEvent.session.id) + .hasView( + fakeViewEvent.view.id, + fakeViewEvent.view.name, + fakeViewEvent.view.url + ) + .hasMessage(ANRDetectorRunnable.ANR_MESSAGE) + .hasStackTrace(fakeThreadsDump.first { it.name == "main" }.stack) + .isCrash(true) + .hasErrorSource(RumErrorSource.SOURCE) + .hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + .hasTimestamp(fakeTimestamp + fakeServerOffset) + .hasNoUserInfo() + .hasErrorType(ANRException::class.java.canonicalName) + .hasErrorCategory(ErrorEvent.Category.ANR) + .hasLiteSessionPlan() + .hasDeviceInfo( + fakeDatadogContext.deviceInfo.deviceName, + fakeDatadogContext.deviceInfo.deviceModel, + fakeDatadogContext.deviceInfo.deviceBrand, + fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), + fakeDatadogContext.deviceInfo.architecture + ) + .hasOsInfo( + fakeDatadogContext.deviceInfo.osName, + fakeDatadogContext.deviceInfo.osVersion, + fakeDatadogContext.deviceInfo.osMajorVersion + ) + .hasThreads(fakeThreadsDump) + + ViewEventAssert.assertThat(secondValue as ViewEvent) + .hasVersion(fakeViewEvent.dd.documentVersion + 1) + .hasCrashCount((fakeViewEvent.view.crash?.count ?: 0) + 1) + .isActive(false) + } + + verify(mockSdkCore).writeLastFatalAnrSent(fakeTimestamp) + } + + @Test + fun `𝕄 send only RUM error 𝕎 handleAnrCrash() { view is too old }`( + @LongForgery(min = 1) fakeTimestamp: Long, + @Forgery viewEvent: ViewEvent, + @Forgery fakeUserInfo: UserInfo, + forge: Forge + ) { + // Given + val fakeServerOffset = + forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerOffset + ) + ) + + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD + 1 + ), + usr = ViewEvent.Usr( + id = fakeUserInfo.id, + name = fakeUserInfo.name, + email = fakeUserInfo.email, + additionalProperties = fakeUserInfo.additionalProperties.toMutableMap() + ) + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) doReturn fakeViewEvent + + val fakeThreadsDump = forge.anrCrashThreadDump() + whenever(mockAndroidTraceParser.parse(any())) doReturn fakeThreadsDump + val mockAnrExitInfo = mock().apply { + whenever(traceInputStream) doReturn mock() + whenever(timestamp) doReturn fakeTimestamp + } + + // When + testedHandler.handleAnrCrash(mockAnrExitInfo, fakeViewEventJson, mockRumWriter) + + // Then + argumentCaptor { + verify(mockRumWriter, times(1)).write(eq(mockEventBatchWriter), capture()) + + ErrorEventAssert.assertThat(firstValue as ErrorEvent) + .hasApplicationId(fakeViewEvent.application.id) + .hasSessionId(fakeViewEvent.session.id) + .hasView( + fakeViewEvent.view.id, + fakeViewEvent.view.name, + fakeViewEvent.view.url + ) + .hasMessage(ANRDetectorRunnable.ANR_MESSAGE) + .hasStackTrace(fakeThreadsDump.first { it.name == "main" }.stack) + .isCrash(true) + .hasErrorSource(RumErrorSource.SOURCE) + .hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + .hasTimestamp(fakeTimestamp + fakeServerOffset) + .hasUserInfo( + UserInfo( + fakeViewEvent.usr?.id, + fakeViewEvent.usr?.name, + fakeViewEvent.usr?.email, + fakeViewEvent.usr?.additionalProperties.orEmpty() + ) + ) + .hasErrorType(ANRException::class.java.canonicalName) + .hasErrorCategory(ErrorEvent.Category.ANR) + .hasLiteSessionPlan() + .hasDeviceInfo( + fakeDatadogContext.deviceInfo.deviceName, + fakeDatadogContext.deviceInfo.deviceModel, + fakeDatadogContext.deviceInfo.deviceBrand, + fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), + fakeDatadogContext.deviceInfo.architecture + ) + .hasOsInfo( + fakeDatadogContext.deviceInfo.osName, + fakeDatadogContext.deviceInfo.osVersion, + fakeDatadogContext.deviceInfo.osMajorVersion + ) + .hasThreads(fakeThreadsDump) + } + + verify(mockSdkCore).writeLastFatalAnrSent(fakeTimestamp) + } + + @Test + fun `𝕄 log warning and not send anything 𝕎 handleAnrCrash() { RUM feature not registered }`( + @LongForgery(min = 1) fakeTimestamp: Long, + @Forgery viewEvent: ViewEvent, + forge: Forge + ) { + // Given + val fakeServerOffset = + forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerOffset + ) + ) + + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = 0L, + max = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 + ) + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) doReturn fakeViewEvent + + val mockAnrExitInfo = mock().apply { + whenever(traceInputStream) doReturn mock() + whenever(timestamp) doReturn fakeTimestamp + } + + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null + + // When + testedHandler.handleAnrCrash(mockAnrExitInfo, fakeViewEventJson, mockRumWriter) + + // Then + verifyNoInteractions(mockRumWriter) + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + DatadogLateCrashReporter.INFO_RUM_FEATURE_NOT_REGISTERED + ) + } + + @Test + fun `𝕄 not send anything 𝕎 handleAnrCrash() { view deserialization fails }`( + @LongForgery(min = 1) fakeTimestamp: Long, + @Forgery viewEvent: ViewEvent + ) { + // Given + val fakeViewEventJson = viewEvent.toJson().asJsonObject + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) doReturn null + + val mockAnrExitInfo = mock().apply { + whenever(traceInputStream) doReturn mock() + whenever(timestamp) doReturn fakeTimestamp + } + + // When + testedHandler.handleAnrCrash(mockAnrExitInfo, fakeViewEventJson, mockRumWriter) + + // Then + verifyNoInteractions(mockRumWriter, mockAndroidTraceParser) + } + + @Test + fun `𝕄 not send anything 𝕎 handleAnrCrash() { Crash timestamp is before last RUM view }`( + @Forgery viewEvent: ViewEvent, + forge: Forge + ) { + // Given + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = 0L, + max = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 + ) + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) doReturn fakeViewEvent + + val mockAnrExitInfo = mock().apply { + whenever(traceInputStream) doReturn mock() + whenever(timestamp) doReturn fakeViewEvent.date - 1 + } + + // When + testedHandler.handleAnrCrash(mockAnrExitInfo, fakeViewEventJson, mockRumWriter) + + // Then + verifyNoInteractions(mockRumWriter, mockInternalLogger) + } + + @Test + fun `𝕄 not send anything 𝕎 handleAnrCrash() { last view event belongs to the current session }`( + @LongForgery(min = 1) fakeTimestamp: Long, + @Forgery viewEvent: ViewEvent, + forge: Forge + ) { + // Given + val fakeServerOffset = + forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerOffset + ), + featuresContext = mapOf( + Feature.RUM_FEATURE_NAME to mapOf(RumContext.SESSION_ID to viewEvent.session.id) + ) + ) + + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = 0L, + max = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 + ) + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) doReturn fakeViewEvent + + val mockAnrExitInfo = mock().apply { + whenever(traceInputStream) doReturn mock() + whenever(timestamp) doReturn fakeTimestamp + } + + // When + testedHandler.handleAnrCrash(mockAnrExitInfo, fakeViewEventJson, mockRumWriter) + + // Then + verifyNoInteractions(mockRumWriter) + } + + @Test + fun `𝕄 not send anything 𝕎 handleAnrCrash() { ANR was already sent }`( + @LongForgery(min = 1) fakeTimestamp: Long, + @Forgery viewEvent: ViewEvent, + forge: Forge + ) { + // Given + val fakeServerOffset = + forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerOffset + ) + ) + + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = 0L, + max = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 + ) + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) doReturn fakeViewEvent + + val mockAnrExitInfo = mock().apply { + whenever(traceInputStream) doReturn mock() + whenever(timestamp) doReturn fakeTimestamp + } + whenever(mockSdkCore.lastFatalAnrSent) doReturn fakeTimestamp + + // When + testedHandler.handleAnrCrash(mockAnrExitInfo, fakeViewEventJson, mockRumWriter) + + // Then + verifyNoInteractions(mockRumWriter) + } + + @Test + fun `𝕄 not send anything 𝕎 handleAnrCrash() { empty threads dump }`( + @LongForgery(min = 1) fakeTimestamp: Long, + @Forgery viewEvent: ViewEvent, + forge: Forge + ) { + // Given + val fakeServerOffset = + forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) + fakeDatadogContext = fakeDatadogContext.copy( + time = fakeDatadogContext.time.copy( + serverTimeOffsetMs = fakeServerOffset + ) + ) + + val fakeViewEvent = viewEvent.copy( + date = System.currentTimeMillis() - forge.aLong( + min = 0L, + max = DatadogLateCrashReporter.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 + ) + ) + val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject + + whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) doReturn fakeViewEvent + + val mockAnrExitInfo = mock().apply { + whenever(traceInputStream) doReturn mock() + whenever(timestamp) doReturn fakeTimestamp + } + whenever(mockAndroidTraceParser.parse(any())) doReturn emptyList() + + // When + testedHandler.handleAnrCrash(mockAnrExitInfo, fakeViewEventJson, mockRumWriter) + + // Then + verifyNoInteractions(mockRumWriter) + } + + // endregion + + private fun Forge.anrCrashThreadDump(): List { + val otherThreads = aList { getForgery() }.map { it.copy(crashed = false) } + val mainThread = getForgery().copy(name = "main", crashed = true) + return shuffle(otherThreads + mainThread) + } + + enum class ValueMissingType { + MISSING, + NULL, + WRONG_TYPE + } +} diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/DefaultAppStartTimeProviderTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/DefaultAppStartTimeProviderTest.kt index 3e58e6cdf6..8273db488e 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/DefaultAppStartTimeProviderTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/DefaultAppStartTimeProviderTest.kt @@ -32,7 +32,7 @@ class DefaultAppStartTimeProviderTest { ) { // GIVEN val mockBuildSdkVersionProvider: BuildSdkVersionProvider = mock() - whenever(mockBuildSdkVersionProvider.version()) doReturn apiVersion + whenever(mockBuildSdkVersionProvider.version) doReturn apiVersion val diffMs = SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime() val startTimeNs = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(diffMs) @@ -51,7 +51,7 @@ class DefaultAppStartTimeProviderTest { ) { // GIVEN val mockBuildSdkVersionProvider: BuildSdkVersionProvider = mock() - whenever(mockBuildSdkVersionProvider.version()) doReturn apiVersion + whenever(mockBuildSdkVersionProvider.version) doReturn apiVersion val startTimeNs = RumFeature.startupTimeNs // WHEN 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 0abe55a85a..4b9b479dc5 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 @@ -6,12 +6,16 @@ package com.datadog.android.rum.internal +import android.app.ActivityManager import android.app.Application +import android.app.ApplicationExitInfo +import android.content.Context import android.os.Build import com.datadog.android.api.InternalLogger import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.feature.event.JvmCrash import com.datadog.android.core.feature.event.ThreadDump +import com.datadog.android.core.internal.system.BuildSdkVersionProvider import com.datadog.android.event.EventMapper import com.datadog.android.event.MapperSerializer import com.datadog.android.rum.GlobalRumMonitor @@ -22,7 +26,6 @@ import com.datadog.android.rum.configuration.VitalsUpdateFrequency import com.datadog.android.rum.internal.domain.RumDataWriter import com.datadog.android.rum.internal.domain.event.RumEventMapper import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.internal.ndk.NdkCrashEventHandler import com.datadog.android.rum.internal.storage.NoOpDataWriter import com.datadog.android.rum.internal.thread.NoOpScheduledExecutorService import com.datadog.android.rum.internal.tracking.NoOpUserActionTrackingStrategy @@ -48,6 +51,7 @@ import com.datadog.tools.unit.extensions.ApiLevelExtension import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration import com.datadog.tools.unit.forge.aThrowable +import com.datadog.tools.unit.forge.anException import com.datadog.tools.unit.forge.exhaustiveAttributes import com.datadog.tools.unit.getFieldValue import com.google.gson.JsonObject @@ -70,7 +74,10 @@ import org.junit.jupiter.params.provider.EnumSource 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.doAnswer import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions @@ -79,6 +86,7 @@ import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.util.Locale import java.util.UUID +import java.util.concurrent.ExecutorService import java.util.concurrent.ScheduledThreadPoolExecutor @Extensions( @@ -109,7 +117,7 @@ internal class RumFeatureTest { lateinit var mockInternalLogger: InternalLogger @Mock - lateinit var mockNdkCrashEventHandler: NdkCrashEventHandler + lateinit var mockLateCrashReporter: LateCrashReporter @BeforeEach fun `set up`() { @@ -119,7 +127,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) GlobalRumMonitor.registerIfAbsent(mockRumMonitor, mockSdkCore) } @@ -197,7 +205,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -217,7 +225,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -244,7 +252,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -266,7 +274,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -290,7 +298,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -313,7 +321,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -342,7 +350,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -373,7 +381,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration.copy(viewTrackingStrategy = null), - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -392,7 +400,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -411,7 +419,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -429,7 +437,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -468,7 +476,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -498,7 +506,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -602,7 +610,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -623,7 +631,7 @@ internal class RumFeatureTest { mockSdkCore, fakeApplicationId.toString(), fakeConfiguration, - ndkCrashEventHandlerFactory = { mockNdkCrashEventHandler } + lateCrashReporterFactory = { mockLateCrashReporter } ) // When @@ -634,6 +642,48 @@ internal class RumFeatureTest { .isInstanceOf(NoOpScheduledExecutorService::class.java) } + @Test + fun `𝕄 initialize non-fatal ANR tracking 𝕎 initialize { trackNonFatalAnrs = true }()`() { + // Given + fakeConfiguration = fakeConfiguration.copy( + trackNonFatalAnrs = true + ) + testedFeature = RumFeature( + mockSdkCore, + fakeApplicationId.toString(), + fakeConfiguration, + lateCrashReporterFactory = { mockLateCrashReporter } + ) + + // When + testedFeature.onInitialize(appContext.mockInstance) + + // Then + assertThat(testedFeature.anrDetectorRunnable) + .isNotNull() + } + + @Test + fun `𝕄 not initialize non-fatal ANR tracking 𝕎 initialize { trackNonFatalAnrs = false }()`() { + // Given + fakeConfiguration = fakeConfiguration.copy( + trackNonFatalAnrs = false + ) + testedFeature = RumFeature( + mockSdkCore, + fakeApplicationId.toString(), + fakeConfiguration, + lateCrashReporterFactory = { mockLateCrashReporter } + ) + + // When + testedFeature.onInitialize(appContext.mockInstance) + + // Then + assertThat(testedFeature.anrDetectorRunnable) + .isNull() + } + @Test fun `𝕄 shut down vital executor 𝕎 onStop()`() { // Given @@ -831,10 +881,9 @@ internal class RumFeatureTest { testedFeature.onReceive(event) // Then - verify(mockNdkCrashEventHandler) - .handleEvent( + verify(mockLateCrashReporter) + .handleNdkCrashEvent( event, - mockSdkCore, testedFeature.dataWriter ) @@ -844,6 +893,153 @@ internal class RumFeatureTest { ) } + @Test + fun `𝕄 consume last fatal ANR crash 𝕎 consumeLastFatalAnr()`( + @Forgery fakeViewEventJson: JsonObject, + forge: Forge + ) { + // Given + val appExitInfo = forge.anApplicationExitInfoList(mustInclude = ApplicationExitInfo.REASON_ANR) + + val mockActivityManager = mock() + whenever( + mockActivityManager.getHistoricalProcessExitReasons(null, 0, 0) + ) doReturn appExitInfo + whenever( + appContext.mockInstance.getSystemService(Context.ACTIVITY_SERVICE) + ) doReturn mockActivityManager + val mockExecutor = mockSameThreadExecutorService() + whenever(mockSdkCore.lastViewEvent) doReturn fakeViewEventJson + testedFeature.onInitialize(appContext.mockInstance) + + // When + testedFeature.consumeLastFatalAnr(mockExecutor) + + // Then + verify(mockLateCrashReporter) + .handleAnrCrash( + appExitInfo.first { it.reason == ApplicationExitInfo.REASON_ANR }, + fakeViewEventJson, + testedFeature.dataWriter + ) + } + + @Test + fun `𝕄 not consume last fatal ANR crash 𝕎 consumeLastFatalAnr() { no last view event }`( + forge: Forge + ) { + // Given + val appExitInfo = forge.anApplicationExitInfoList(mustInclude = ApplicationExitInfo.REASON_ANR) + + val mockActivityManager = mock() + whenever( + mockActivityManager.getHistoricalProcessExitReasons(null, 0, 0) + ) doReturn appExitInfo + whenever( + appContext.mockInstance.getSystemService(Context.ACTIVITY_SERVICE) + ) doReturn mockActivityManager + val mockExecutor = mockSameThreadExecutorService() + whenever(mockSdkCore.lastViewEvent) doReturn null + testedFeature.onInitialize(appContext.mockInstance) + + // When + testedFeature.consumeLastFatalAnr(mockExecutor) + + // Then + verifyNoInteractions(mockLateCrashReporter) + mockInternalLogger.verifyLog( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + RumFeature.NO_LAST_RUM_VIEW_EVENT_AVAILABLE + ) + } + + @Test + fun `𝕄 not consume last fatal ANR crash 𝕎 consumeLastFatalAnr() { no known ANR exit }`( + @Forgery fakeViewEventJson: JsonObject, + forge: Forge + ) { + // Given + val appExitInfo = forge.anApplicationExitInfoList() + .filter { it.reason != ApplicationExitInfo.REASON_ANR } + + val mockActivityManager = mock() + whenever( + mockActivityManager.getHistoricalProcessExitReasons(null, 0, 0) + ) doReturn appExitInfo + whenever( + appContext.mockInstance.getSystemService(Context.ACTIVITY_SERVICE) + ) doReturn mockActivityManager + val mockExecutor = mockSameThreadExecutorService() + whenever(mockSdkCore.lastViewEvent) doReturn fakeViewEventJson + testedFeature.onInitialize(appContext.mockInstance) + + // When + testedFeature.consumeLastFatalAnr(mockExecutor) + + // Then + verifyNoInteractions(mockLateCrashReporter, mockInternalLogger) + } + + @Test + fun `𝕄 log error 𝕎 consumeLastFatalAnr() { error getting historical exit reasons }`( + forge: Forge + ) { + // Given + val mockActivityManager = mock() + val exceptionThrown = forge.anException() + whenever( + mockActivityManager.getHistoricalProcessExitReasons(null, 0, 0) + ) doThrow exceptionThrown + whenever( + appContext.mockInstance.getSystemService(Context.ACTIVITY_SERVICE) + ) doReturn mockActivityManager + val mockExecutor = mockSameThreadExecutorService() + testedFeature.onInitialize(appContext.mockInstance) + + // When + testedFeature.consumeLastFatalAnr(mockExecutor) + + // Then + verifyNoInteractions(mockLateCrashReporter) + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + RumFeature.FAILED_TO_GET_HISTORICAL_EXIT_REASONS, + exceptionThrown + ) + } + + @Test + fun `𝕄 return true 𝕎 isTrackNonFatalAnrsEnabledByDefault() { Android Q and below }`( + @IntForgery(min = 1, max = Build.VERSION_CODES.R) fakeBuildSdkVersion: Int + ) { + // Given + val mockBuildSdkVersionProvider = mock() + whenever(mockBuildSdkVersionProvider.version) doReturn fakeBuildSdkVersion + + // When + val isEnabled = RumFeature.isTrackNonFatalAnrsEnabledByDefault(mockBuildSdkVersionProvider) + + // Then + assertThat(isEnabled).isTrue() + } + + @Test + fun `𝕄 return false 𝕎 isTrackNonFatalAnrsEnabledByDefault() { Android R and above }`( + @IntForgery(min = Build.VERSION_CODES.R) fakeBuildSdkVersion: Int + ) { + // Given + val mockBuildSdkVersionProvider = mock() + whenever(mockBuildSdkVersionProvider.version) doReturn fakeBuildSdkVersion + + // When + val isEnabled = RumFeature.isTrackNonFatalAnrsEnabledByDefault(mockBuildSdkVersionProvider) + + // Then + assertThat(isEnabled).isFalse() + } + // region FeatureEventReceiver#onReceive + logger error @Test @@ -1241,6 +1437,54 @@ internal class RumFeatureTest { // endregion + private fun Forge.anApplicationExitInfoList( + mustInclude: Int? = null + ): List { + val appExitInfos = aList { + mock().apply { + whenever(reason) doReturn anApplicationExitInfoReason() + } + }.toMutableList() + if (mustInclude != null && !appExitInfos.any { it.reason == mustInclude }) { + appExitInfos[anElementFrom(appExitInfos.indices.toList())] = + mock().apply { + whenever(reason) doReturn mustInclude + } + } + return appExitInfos + } + + private fun Forge.anApplicationExitInfoReason(): Int { + return anElementFrom( + ApplicationExitInfo.REASON_UNKNOWN, + ApplicationExitInfo.REASON_EXIT_SELF, + ApplicationExitInfo.REASON_SIGNALED, + ApplicationExitInfo.REASON_LOW_MEMORY, + ApplicationExitInfo.REASON_CRASH, + ApplicationExitInfo.REASON_CRASH_NATIVE, + ApplicationExitInfo.REASON_ANR, + ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, + ApplicationExitInfo.REASON_PERMISSION_CHANGE, + ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE, + ApplicationExitInfo.REASON_USER_REQUESTED, + ApplicationExitInfo.REASON_USER_STOPPED, + ApplicationExitInfo.REASON_DEPENDENCY_DIED, + ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.REASON_FREEZER, + ApplicationExitInfo.REASON_PACKAGE_STATE_CHANGE, + ApplicationExitInfo.REASON_PACKAGE_UPDATED + ) + } + + private fun mockSameThreadExecutorService(): ExecutorService { + return mock().apply { + whenever(submit(any())) doAnswer { + it.getArgument(0).run() + mock() + } + } + } + companion object { val appContext = ApplicationContextTestConfiguration(Application::class.java) private val mainLooper = MainLooperTestConfiguration() diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/anr/ANRDetectorRunnableTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/anr/ANRDetectorRunnableTest.kt index 9d4513a7a3..eef45f6762 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/anr/ANRDetectorRunnableTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/anr/ANRDetectorRunnableTest.kt @@ -9,6 +9,9 @@ package com.datadog.android.rum.internal.anr import android.content.Context import android.os.Handler import android.os.Looper +import com.datadog.android.core.feature.event.ThreadDump +import com.datadog.android.core.internal.utils.loggableStackTrace +import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.utils.config.ApplicationContextTestConfiguration import com.datadog.android.rum.utils.config.GlobalRumMonitorTestConfiguration @@ -36,7 +39,6 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.isA import org.mockito.kotlin.reset -import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.verifyNoMoreInteractions @@ -52,7 +54,7 @@ import org.mockito.quality.Strictness @ForgeConfiguration(value = Configurator::class) internal class ANRDetectorRunnableTest { - lateinit var testedRunnable: ANRDetectorRunnable + private lateinit var testedRunnable: ANRDetectorRunnable @Mock lateinit var mockHandler: Handler @@ -82,12 +84,25 @@ internal class ANRDetectorRunnableTest { // Then Thread.sleep(TEST_ANR_THRESHOLD_MS) - verify(rumMonitor.mockInstance).addError( - eq("Application Not Responding"), - eq(RumErrorSource.SOURCE), - any(), - eq(emptyMap()) - ) + argumentCaptor> { + val anrExceptionCaptor = argumentCaptor() + verify(rumMonitor.mockInstance).addError( + message = eq("Application Not Responding"), + source = eq(RumErrorSource.SOURCE), + throwable = anrExceptionCaptor.capture(), + attributes = capture() + ) + assertThat(anrExceptionCaptor.lastValue).isInstanceOf(ANRException::class.java) + + assertThat(lastValue).containsOnlyKeys(RumAttributes.INTERNAL_ALL_THREADS) + @Suppress("UNCHECKED_CAST") + val allThreads = lastValue[RumAttributes.INTERNAL_ALL_THREADS] as List + assertThat(allThreads.all { !it.crashed }).isTrue() + + val anrThread = allThreads.firstOrNull { it.name == Thread.currentThread().name } + check(anrThread != null) + assertThat(anrThread.stack).isEqualTo(anrExceptionCaptor.lastValue.loggableStackTrace()) + } argumentCaptor { verify(mockHandler).post(capture()) @@ -128,7 +143,7 @@ internal class ANRDetectorRunnableTest { lastValue.run() // +10 is to remove flakiness, otherwise it seems current thread can resume execution - // before AND test thread + // before ANR test thread Thread.sleep(TEST_ANR_TEST_DELAY_MS + 10) verify(mockHandler).post(capture()) lastValue.run() diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/anr/AndroidTraceParserTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/anr/AndroidTraceParserTest.kt new file mode 100644 index 0000000000..ea7f2632e8 --- /dev/null +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/anr/AndroidTraceParserTest.kt @@ -0,0 +1,304 @@ +/* + * 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.anr + +import com.datadog.android.api.InternalLogger +import com.datadog.android.rum.utils.forge.Configurator +import com.datadog.android.rum.utils.verifyLog +import fr.xgouchet.elmyr.annotation.StringForgery +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.Mockito.mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.IOException +import java.io.InputStream + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class AndroidTraceParserTest { + + @Mock + lateinit var mockInternalLogger: InternalLogger + + private lateinit var testedParser: AndroidTraceParser + + @BeforeEach + fun `set up`() { + testedParser = AndroidTraceParser(mockInternalLogger) + } + + @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + @Test + fun `𝕄 return threads dump 𝕎 parse()`() { + // Given + val traceStream = javaClass.classLoader.getResourceAsStream("anr_crash_trace.txt") + + // When + val threadsDump = testedParser.parse(traceStream) + + // Then + assertThat(threadsDump).isNotEmpty + assertThat(threadsDump.filter { it.crashed }).hasSize(1) + assertThat(threadsDump).allMatch { it.stack.isNotEmpty() } + + assertThat(threadsDump.filter { it.name == "main" }).hasSize(1) + val mainThread = threadsDump.first { it.name == "main" } + assertThat(mainThread.stack).isEqualTo(MAIN_THREAD_STACK) + assertThat(mainThread.state).isEqualTo("runnable") + assertThat(mainThread.crashed).isTrue() + + assertThat(threadsDump.filter { it.name == "OkHttp browser-intake-datadoghq.com" }) + .hasSize(1) + val mixedJavaNativeThread = + threadsDump.first { it.name == "OkHttp browser-intake-datadoghq.com" } + assertThat(mixedJavaNativeThread.stack).isEqualTo(JAVA_AND_NDK_THREAD_STACK) + assertThat(mixedJavaNativeThread.state).isEqualTo("native") + assertThat(mixedJavaNativeThread.crashed).isFalse() + } + + @Test + fun `𝕄 return empty list 𝕎 parse() { malformed trace }`( + @StringForgery fakeTrace: String + ) { + // When + val threadsDump = testedParser.parse(fakeTrace.byteInputStream()) + + // Then + assertThat(threadsDump).isEmpty() + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + message = AndroidTraceParser.PARSING_FAILURE_MESSAGE + ) + } + + @Test + fun `𝕄 return empty list 𝕎 parse() { error reading stream }`() { + // When + val mockStream = mock().apply { + whenever(read()) doThrow IOException() + } + val threadsDump = testedParser.parse(mockStream) + + // Then + assertThat(threadsDump).isEmpty() + + mockInternalLogger.verifyLog( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + AndroidTraceParser.TRACE_STREAM_READ_FAILURE, + throwableClass = IOException::class.java + ) + } + + companion object { + const val MAIN_THREAD_STACK = + """ at android.graphics.Paint.getNativeInstance(Paint.java:743) + at android.graphics.BaseRecordingCanvas.drawRect(BaseRecordingCanvas.java:364) + at com.datadog.android.sample.vitals.BadView.onDraw(BadView.kt:72) + at android.view.View.draw(View.java:23889) + at android.view.View.updateDisplayListIfDirty(View.java:22756) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:694) + at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:700) + at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:798) + at android.view.ViewRootImpl.draw(ViewRootImpl.java:4939) + at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:4643) + at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3822) + at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2465) + at android.view.ViewRootImpl${'$'}TraversalRunnable.run(ViewRootImpl.java:9305) + at android.view.Choreographer${'$'}CallbackRecord.run(Choreographer.java:1339) + at android.view.Choreographer${'$'}CallbackRecord.run(Choreographer.java:1348) + at android.view.Choreographer.doCallbacks(Choreographer.java:952) + at android.view.Choreographer.doFrame(Choreographer.java:882) + at android.view.Choreographer${'$'}FrameDisplayEventReceiver.run(Choreographer.java:1322) + at android.os.Handler.handleCallback(Handler.java:958) + at android.os.Handler.dispatchMessage(Handler.java:99) + at android.os.Looper.loopOnce(Looper.java:205) + at android.os.Looper.loop(Looper.java:294) + at android.app.ActivityThread.main(ActivityThread.java:8177) + at java.lang.reflect.Method.invoke(Native method) + at com.android.internal.os.RuntimeInit${'$'}MethodAndArgsCaller.run(RuntimeInit.java:552) + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)""" + + const val JAVA_AND_NDK_THREAD_STACK = + """ native: #00 pc 00062e1c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 0022cfac /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks+140) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #02 pc 0039978c /apex/com.android.art/lib64/libart.so (art::::CheckJNI::SetPrimitiveArrayRegion +1352) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #03 pc 0002de34 /apex/com.android.art/lib64/libopenjdk.so (SocketInputStream_socketRead0+260) (BuildId: fc4c0ac2dde70b1afe348b962a85a634) + native: #04 pc 00377030 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #05 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #06 pc 003ae360 /apex/com.android.art/lib64/libart.so (art::interpreter::ArtInterpreterToCompiledCodeBridge+320) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #07 pc 00398584 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1488) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #08 pc 0050cf2c /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+12964) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #09 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #10 pc 00145c98 /apex/com.android.art/javalib/core-oj.jar (java.net.SocketInputStream.socketRead) + native: #11 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #12 pc 00398d78 /apex/com.android.art/lib64/libart.so (art::interpreter::ArtInterpreterToInterpreterBridge+100) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #13 pc 00398520 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1388) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #14 pc 0050cf2c /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+12964) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #15 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #16 pc 00145b14 /apex/com.android.art/javalib/core-oj.jar (java.net.SocketInputStream.read) + native: #17 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #18 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #19 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #20 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #21 pc 00145aec /apex/com.android.art/javalib/core-oj.jar (java.net.SocketInputStream.read) + native: #22 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #23 pc 0037c560 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+672) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #24 pc 00377168 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #25 pc 0058acb0 /apex/com.android.art/lib64/libart.so (nterp_helper+4016) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #26 pc 0001818e /apex/com.android.conscrypt/javalib/conscrypt.jar (com.android.org.conscrypt.ConscryptEngineSocket${'$'}SSLInputStream.readFromSocket+50) + native: #27 pc 0058ac54 /apex/com.android.art/lib64/libart.so (nterp_helper+3924) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #28 pc 0001800e /apex/com.android.conscrypt/javalib/conscrypt.jar (com.android.org.conscrypt.ConscryptEngineSocket${'$'}SSLInputStream.processDataFromSocket+350) + native: #29 pc 0058ac54 /apex/com.android.art/lib64/libart.so (nterp_helper+3924) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #30 pc 000181d2 /apex/com.android.conscrypt/javalib/conscrypt.jar (com.android.org.conscrypt.ConscryptEngineSocket${'$'}SSLInputStream.readUntilDataAvailable+2) + native: #31 pc 0058ac54 /apex/com.android.art/lib64/libart.so (nterp_helper+3924) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #32 pc 0001812c /apex/com.android.conscrypt/javalib/conscrypt.jar (com.android.org.conscrypt.ConscryptEngineSocket${'$'}SSLInputStream.read+16) + native: #33 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #34 pc 004906b4 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1248) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #35 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #36 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #37 pc 0008a120 (okio.InputStreamSource.read) + native: #38 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #39 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #40 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #41 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #42 pc 0007f0f0 (okio.AsyncTimeout${'$'}source${'$'}1.read) + native: #43 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #44 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #45 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #46 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #47 pc 0008f1a4 (okio.RealBufferedSource.request) + native: #48 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #49 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #50 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #51 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #52 pc 000903ec (okio.RealBufferedSource.require) + native: #53 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #54 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #55 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #56 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #57 pc 00071904 (okhttp3.internal.http2.Http2Reader.nextFrame) + native: #58 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #59 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #60 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #61 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #62 pc 0006f0a8 (okhttp3.internal.http2.Http2Connection${'$'}ReaderRunnable.invoke) + native: #63 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #64 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #65 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #66 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #67 pc 0006eacc (okhttp3.internal.http2.Http2Connection${'$'}ReaderRunnable.invoke) + native: #68 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #69 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #70 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #71 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #72 pc 00062510 (okhttp3.internal.concurrent.TaskQueue${'$'}execute${'$'}1.runOnce) + native: #73 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #74 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #75 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #76 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #77 pc 00063868 (okhttp3.internal.concurrent.TaskRunner.runTask) + native: #78 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #79 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #80 pc 0050a5d4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+2380) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #81 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #82 pc 000634d8 (okhttp3.internal.concurrent.TaskRunner.access${'$'}runTask) + native: #83 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #84 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #85 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #86 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #87 pc 00062fd0 (okhttp3.internal.concurrent.TaskRunner${'$'}runnable${'$'}1.run) + native: #88 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #89 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #90 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #91 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #92 pc 002488d8 /apex/com.android.art/javalib/core-oj.jar (java.util.concurrent.ThreadPoolExecutor.runWorker) + native: #93 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #94 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #95 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #96 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #97 pc 00247774 /apex/com.android.art/javalib/core-oj.jar (java.util.concurrent.ThreadPoolExecutor${'$'}Worker.run) + native: #98 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #99 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #100 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #101 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #102 pc 0000308c [anon:dalvik-/apex/com.android.art/javalib/core-oj.jar-transformed] (java.lang.Thread.run) + native: #103 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #104 pc 0037c560 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+672) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #105 pc 00377168 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #106 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #107 pc 0034b8a4 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #108 pc 004f3e30 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback+1888) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #109 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #110 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + at java.net.SocketInputStream.socketRead0(Native method) + at java.net.SocketInputStream.socketRead(SocketInputStream.java:118) + at java.net.SocketInputStream.read(SocketInputStream.java:173) + at java.net.SocketInputStream.read(SocketInputStream.java:143) + at com.android.org.conscrypt.ConscryptEngineSocket${'$'}SSLInputStream.readFromSocket(ConscryptEngineSocket.java:983) + at com.android.org.conscrypt.ConscryptEngineSocket${'$'}SSLInputStream.processDataFromSocket(ConscryptEngineSocket.java:947) + at com.android.org.conscrypt.ConscryptEngineSocket${'$'}SSLInputStream.readUntilDataAvailable(ConscryptEngineSocket.java:862) + at com.android.org.conscrypt.ConscryptEngineSocket${'$'}SSLInputStream.read(ConscryptEngineSocket.java:835) + at okio.InputStreamSource.read(JvmOkio.kt:94) + at okio.AsyncTimeout${'$'}source${'$'}1.read(AsyncTimeout.kt:125) + at okio.RealBufferedSource.request(RealBufferedSource.kt:206) + at okio.RealBufferedSource.require(RealBufferedSource.kt:199) + at okhttp3.internal.http2.Http2Reader.nextFrame(Http2Reader.kt:89) + at okhttp3.internal.http2.Http2Connection${'$'}ReaderRunnable.invoke(Http2Connection.kt:618) + at okhttp3.internal.http2.Http2Connection${'$'}ReaderRunnable.invoke(Http2Connection.kt:609) + at okhttp3.internal.concurrent.TaskQueue${'$'}execute${'$'}1.runOnce(TaskQueue.kt:98) + at okhttp3.internal.concurrent.TaskRunner.runTask(TaskRunner.kt:116) + at okhttp3.internal.concurrent.TaskRunner.access${'$'}runTask(TaskRunner.kt:42) + at okhttp3.internal.concurrent.TaskRunner${'$'}runnable${'$'}1.run(TaskRunner.kt:65) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) + at java.util.concurrent.ThreadPoolExecutor${'$'}Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012)""" + } +} 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 4edbf0c109..27133f06b1 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 @@ -6,6 +6,7 @@ package com.datadog.android.rum.internal.domain.scope +import android.app.ActivityManager import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.context.TimeInfo import com.datadog.android.api.feature.Feature @@ -13,8 +14,10 @@ import com.datadog.android.api.feature.FeatureScope import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver +import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumSessionListener +import com.datadog.android.rum.internal.AppStartTimeProvider import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.vitals.VitalMonitor import com.datadog.android.rum.utils.forge.Configurator @@ -44,6 +47,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.mockito.quality.Strictness +import java.util.concurrent.TimeUnit @Extensions( ExtendWith(MockitoExtension::class), @@ -67,6 +71,9 @@ internal class RumApplicationScopeTest { @Mock lateinit var mockResolver: FirstPartyHostHeaderTypeResolver + @Mock + lateinit var mockAppStartTimeProvider: AppStartTimeProvider + @Mock lateinit var mockCpuVitalMonitor: VitalMonitor @@ -119,7 +126,8 @@ internal class RumApplicationScopeTest { mockCpuVitalMonitor, mockMemoryVitalMonitor, mockFrameRateVitalMonitor, - mockSessionListener + mockSessionListener, + mockAppStartTimeProvider ) } @@ -362,4 +370,133 @@ internal class RumApplicationScopeTest { assertThat(rumContext["view_id"]).isNotNull } } + + @Test + fun `M send ApplicationStarted event once W handleEvent { app is in foreground }`( + forge: Forge + ) { + // Given + val fakeEvents = forge.aList { + forge.anyRumEvent( + excluding = listOf( + RumRawEvent.ApplicationStarted::class.java, + RumRawEvent.SdkInit::class.java + ) + ) + } + val firstEvent = fakeEvents.first() + val appStartTimeNs = forge.aLong(min = 0, max = fakeEvents.first().eventTime.nanoTime) + whenever(mockAppStartTimeProvider.appStartTimeNs) doReturn appStartTimeNs + DdRumContentProvider.processImportance = + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + val mockSessionScope = mock() + testedScope.childScopes.clear() + testedScope.childScopes += mockSessionScope + + val expectedEventTimestamp = + TimeUnit.NANOSECONDS.toMillis( + TimeUnit.MILLISECONDS.toNanos(firstEvent.eventTime.timestamp) - + firstEvent.eventTime.nanoTime + appStartTimeNs + ) + + // When + fakeEvents.forEach { + testedScope.handleEvent(it, mockWriter) + } + + // Then + argumentCaptor { + verify(mockSessionScope).handleEvent(capture(), eq(mockWriter)) + assertThat(firstValue).isInstanceOf(RumRawEvent.ApplicationStarted::class.java) + val appStartEventTime = (firstValue as RumRawEvent.ApplicationStarted).eventTime + assertThat(appStartEventTime.timestamp).isEqualTo(expectedEventTimestamp) + assertThat(appStartEventTime.nanoTime).isEqualTo(appStartTimeNs) + + val processStartTimeNs = + (firstValue as RumRawEvent.ApplicationStarted).applicationStartupNanos + assertThat(processStartTimeNs).isEqualTo(firstEvent.eventTime.nanoTime - appStartTimeNs) + + assertThat(allValues.filterIsInstance()).hasSize(1) + } + } + + @Test + fun `M not send ApplicationStarted event W handleEvent { app is not in foreground }`( + forge: Forge + ) { + // Given + val fakeEvents = forge.aList { + forge.anyRumEvent( + excluding = listOf( + RumRawEvent.ApplicationStarted::class.java, + RumRawEvent.SdkInit::class.java + ) + ) + } + val appStartTimeNs = forge.aLong(min = 0, max = fakeEvents.first().eventTime.nanoTime) + whenever(mockAppStartTimeProvider.appStartTimeNs) doReturn appStartTimeNs + DdRumContentProvider.processImportance = forge.anElementFrom( + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING, + @Suppress("DEPRECATION") + ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING_PRE_28, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE_PRE_26, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE + ) + val mockSessionScope = mock() + testedScope.childScopes.clear() + testedScope.childScopes += mockSessionScope + + // When + fakeEvents.forEach { + testedScope.handleEvent(it, mockWriter) + } + + // Then + argumentCaptor { + verify(mockSessionScope).handleEvent(capture(), eq(mockWriter)) + assertThat(allValues).doesNotHaveSameClassAs(RumRawEvent.ApplicationStarted::class.java) + } + } + + @Test + fun `M not send ApplicationStarted event W handleEvent { SdkInit event }`( + forge: Forge + ) { + // Given + val fakeSdkInitEvent = forge.sdkInitEvent() + val appStartTimeNs = forge.aLong(min = 0, max = fakeSdkInitEvent.eventTime.nanoTime) + whenever(mockAppStartTimeProvider.appStartTimeNs) doReturn appStartTimeNs + DdRumContentProvider.processImportance = forge.anElementFrom( + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING, + @Suppress("DEPRECATION") + ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING_PRE_28, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE_PRE_26, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE + ) + val mockSessionScope = mock() + testedScope.childScopes.clear() + testedScope.childScopes += mockSessionScope + + // When + testedScope.handleEvent(fakeSdkInitEvent, mockWriter) + + // Then + argumentCaptor { + verify(mockSessionScope).handleEvent(capture(), eq(mockWriter)) + assertThat(allValues).doesNotHaveSameClassAs(RumRawEvent.ApplicationStarted::class.java) + } + } } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEventExt.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEventExt.kt index bb0c6cc947..e274b897ae 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEventExt.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEventExt.kt @@ -130,7 +130,32 @@ internal fun Forge.sdkInitEvent(): RumRawEvent.SdkInit { val time = Time() return RumRawEvent.SdkInit( isAppInForeground = aBool(), - appStartTimeNs = aLong(min = 0L, max = time.nanoTime), + eventTime = time + ) +} + +internal fun Forge.updatePerformanceMetricEvent(): RumRawEvent.UpdatePerformanceMetric { + val time = Time() + return RumRawEvent.UpdatePerformanceMetric( + metric = getForgery(), + value = aDouble(), + eventTime = time + ) +} + +internal fun Forge.addFeatureFlagEvaluationEvent(): RumRawEvent.AddFeatureFlagEvaluation { + val time = Time() + return RumRawEvent.AddFeatureFlagEvaluation( + name = anAlphabeticalString(), + value = anElementFrom(aString(), anInt(), Any()), + eventTime = time + ) +} + +internal fun Forge.addCustomTimingEvent(): RumRawEvent.AddCustomTiming { + val time = Time() + return RumRawEvent.AddCustomTiming( + name = anAlphabeticalString(), eventTime = time ) } @@ -168,7 +193,10 @@ internal fun Forge.anyRumEvent(excluding: List = listOf()): RumRawEvent { stopResourceWithErrorEvent(), stopResourceWithStacktraceEvent(), addErrorEvent(), - addLongTaskEvent() + addLongTaskEvent(), + addFeatureFlagEvaluationEvent(), + addCustomTimingEvent(), + updatePerformanceMetricEvent() ) return this.anElementFrom( allEvents.filter { diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt index 9e43c8471a..58d0921f13 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt @@ -1200,6 +1200,7 @@ internal class RumResourceScopeTest { hasActionId(fakeParentContext.actionId) hasErrorType(throwable.javaClass.canonicalName) hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() hasLiteSessionPlan() @@ -1229,6 +1230,85 @@ internal class RumResourceScopeTest { assertThat(result).isEqualTo(null) } + @Test + fun `𝕄 send Error with fingerprint 𝕎 handleEvent(StopResourceWithError) { contains fingerprint }`( + @StringForgery message: String, + @Forgery source: RumErrorSource, + @Forgery throwable: Throwable, + @StringForgery fakeFingerprint: String, + forge: Forge + ) { + // Given + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + + val expectedAttributes = mutableMapOf() + expectedAttributes.putAll(fakeAttributes) + expectedAttributes.putAll(attributes) + + // Expected attributes should not have the ERROR_FINGERPRINT attribute so add it after + attributes[RumAttributes.ERROR_FINGERPRINT] = fakeFingerprint + + mockEvent = RumRawEvent.StopResourceWithError( + fakeKey, + null, + message, + source, + throwable, + attributes + ) + + // When + Thread.sleep(RESOURCE_DURATION_MS) + val result = testedScope.handleEvent(mockEvent, mockWriter) + + // Then + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture()) + assertThat(lastValue) + .apply { + hasMessage(message) + hasErrorSource(source) + hasStackTrace(throwable.loggableStackTrace()) + isCrash(false) + hasResource(fakeUrl, fakeMethod, 0L) + hasUserInfo(fakeDatadogContext.userInfo) + hasConnectivityInfo(fakeNetworkInfoAtScopeStart) + hasView(fakeParentContext) + hasApplicationId(fakeParentContext.applicationId) + hasSessionId(fakeParentContext.sessionId) + hasActionId(fakeParentContext.actionId) + hasErrorType(throwable.javaClass.canonicalName) + hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasUserSession() + hasErrorFingerprint(fakeFingerprint) + hasNoSyntheticsTest() + hasLiteSessionPlan() + hasStartReason(fakeParentContext.sessionStartReason) + hasReplay(fakeHasReplay) + containsExactlyContextAttributes(expectedAttributes) + hasSource(fakeSourceErrorEvent) + hasDeviceInfo( + fakeDatadogContext.deviceInfo.deviceName, + fakeDatadogContext.deviceInfo.deviceModel, + fakeDatadogContext.deviceInfo.deviceBrand, + fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), + fakeDatadogContext.deviceInfo.architecture + ) + hasOsInfo( + fakeDatadogContext.deviceInfo.osName, + fakeDatadogContext.deviceInfo.osVersion, + fakeDatadogContext.deviceInfo.osMajorVersion + ) + hasServiceName(fakeDatadogContext.service) + hasVersion(fakeDatadogContext.version) + hasSampleRate(fakeSampleRate) + } + } + verify(mockParentScope, never()).handleEvent(any(), any()) + verifyNoMoreInteractions(mockWriter) + assertThat(result).isEqualTo(null) + } + @Test fun `𝕄 send Error 𝕎 handleEvent(StopResourceWithStackTrace)`( @StringForgery message: String, @@ -1275,6 +1355,7 @@ internal class RumResourceScopeTest { hasActionId(fakeParentContext.actionId) hasErrorType(errorType) hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() hasLiteSessionPlan() @@ -1366,6 +1447,7 @@ internal class RumResourceScopeTest { hasActionId(fakeParentContext.actionId) hasErrorType(throwable.javaClass.canonicalName) hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasSyntheticsSession() hasSyntheticsTest(fakeTestId, fakeResultId) hasLiteSessionPlan() @@ -1460,6 +1542,7 @@ internal class RumResourceScopeTest { hasActionId(fakeParentContext.actionId) hasErrorType(errorType) hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasSyntheticsSession() hasSyntheticsTest(fakeTestId, fakeResultId) hasLiteSessionPlan() @@ -1549,6 +1632,7 @@ internal class RumResourceScopeTest { hasProviderType(ErrorEvent.ProviderType.FIRST_PARTY) hasErrorType(throwable.javaClass.canonicalName) hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() hasLiteSessionPlan() @@ -1641,6 +1725,7 @@ internal class RumResourceScopeTest { hasProviderType(ErrorEvent.ProviderType.FIRST_PARTY) hasErrorType(errorType) hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() hasLiteSessionPlan() @@ -1717,6 +1802,7 @@ internal class RumResourceScopeTest { hasProviderType(ErrorEvent.ProviderType.FIRST_PARTY) hasErrorType(throwable.javaClass.canonicalName) hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() hasLiteSessionPlan() @@ -1795,6 +1881,7 @@ internal class RumResourceScopeTest { hasProviderType(ErrorEvent.ProviderType.FIRST_PARTY) hasErrorType(errorType) hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() hasLiteSessionPlan() @@ -1869,6 +1956,7 @@ internal class RumResourceScopeTest { hasActionId(fakeParentContext.actionId) hasErrorType(throwable.javaClass.canonicalName) hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() doesNotHaveAResourceProvider() @@ -1947,6 +2035,7 @@ internal class RumResourceScopeTest { hasActionId(fakeParentContext.actionId) hasErrorType(errorType) hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() doesNotHaveAResourceProvider() @@ -2028,6 +2117,7 @@ internal class RumResourceScopeTest { hasActionId(fakeParentContext.actionId) hasErrorType(throwable.javaClass.canonicalName) hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() doesNotHaveAResourceProvider() @@ -2111,6 +2201,7 @@ internal class RumResourceScopeTest { hasActionId(fakeParentContext.actionId) hasErrorType(errorType) hasErrorSourceType(ErrorEvent.SourceType.ANDROID) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() doesNotHaveAResourceProvider() 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 8207af1fe1..147ef12b43 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 @@ -196,44 +196,11 @@ internal class RumSessionScopeTest { } @Test - fun `M send ApplicationStarted event once W handleEvent(SdkInit) { app is in foreground }`( + fun `M not send any event downstream W handleEvent(SdkInit)`( forge: Forge ) { // Given - val fakeEvent = forge.sdkInitEvent().copy(isAppInForeground = true) - - val expectedEventTimestamp = - TimeUnit.NANOSECONDS.toMillis( - TimeUnit.MILLISECONDS.toNanos(fakeEvent.eventTime.timestamp) - - fakeEvent.eventTime.nanoTime + fakeEvent.appStartTimeNs - ) - - // When - testedScope.handleEvent(fakeEvent, mockWriter) - - // Then - argumentCaptor { - verify(mockChildScope).handleEvent(capture(), eq(mockWriter)) - assertThat(firstValue).isInstanceOf(RumRawEvent.ApplicationStarted::class.java) - val appStartEventTime = (firstValue as RumRawEvent.ApplicationStarted).eventTime - assertThat(appStartEventTime.timestamp).isEqualTo(expectedEventTimestamp) - assertThat(appStartEventTime.nanoTime).isEqualTo(fakeEvent.appStartTimeNs) - - val processStartTimeNs = (firstValue as RumRawEvent.ApplicationStarted) - .applicationStartupNanos - assertThat(processStartTimeNs) - .isEqualTo(fakeEvent.eventTime.nanoTime - fakeEvent.appStartTimeNs) - - assertThat(allValues.filterIsInstance()).hasSize(1) - } - } - - @Test - fun `M not send ApplicationStarted event W handleEvent(SdkInit) { app is not in foreground }`( - forge: Forge - ) { - // Given - val fakeEvent = forge.sdkInitEvent().copy(isAppInForeground = false) + val fakeEvent = forge.sdkInitEvent() // When testedScope.handleEvent(fakeEvent, mockWriter) 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 a68e62fd75..d49a7e4a16 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 @@ -29,6 +29,7 @@ import com.datadog.android.rum.assertj.ViewEventAssert.Companion.assertThat import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.internal.RumFeature +import com.datadog.android.rum.internal.anr.ANRException import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor @@ -64,6 +65,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions import org.junit.jupiter.params.ParameterizedTest @@ -91,6 +93,8 @@ import org.mockito.quality.Strictness import java.util.Arrays import java.util.Locale import java.util.UUID +import java.util.concurrent.Executors +import java.util.concurrent.Future import java.util.concurrent.TimeUnit import kotlin.math.max import kotlin.math.min @@ -4376,6 +4380,7 @@ internal class RumViewScopeTest { hasTimestamp(resolveExpectedTimestamp(fakeEvent.eventTime.timestamp)) hasMessage(expectedMessage) hasErrorSource(source) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasStackTrace(stacktrace) isCrash(false) hasNoThreads() @@ -4454,6 +4459,7 @@ internal class RumViewScopeTest { hasTimestamp(resolveExpectedTimestamp(fakeEvent.eventTime.timestamp)) hasMessage(expectedMessage) hasErrorSource(source) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasStackTrace(stacktrace) isCrash(false) hasNoThreads() @@ -4524,6 +4530,79 @@ internal class RumViewScopeTest { hasTimestamp(resolveExpectedTimestamp(fakeEvent.eventTime.timestamp)) hasMessage(message) hasErrorSource(source) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) + hasStackTrace(stacktrace) + isCrash(false) + hasNoThreads() + hasUserInfo(fakeDatadogContext.userInfo) + hasConnectivityInfo(fakeDatadogContext.networkInfo) + hasView(testedScope.viewId, testedScope.key.name, testedScope.url) + hasApplicationId(fakeParentContext.applicationId) + hasSessionId(fakeParentContext.sessionId) + hasActionId(fakeActionId) + hasLiteSessionPlan() + hasStartReason(fakeParentContext.sessionStartReason) + hasReplay(fakeHasReplay) + containsExactlyContextAttributes(attributes) + hasSource(fakeSourceErrorEvent) + hasUserSession() + hasNoSyntheticsTest() + hasDeviceInfo( + fakeDatadogContext.deviceInfo.deviceName, + fakeDatadogContext.deviceInfo.deviceModel, + fakeDatadogContext.deviceInfo.deviceBrand, + fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), + fakeDatadogContext.deviceInfo.architecture + ) + hasOsInfo( + fakeDatadogContext.deviceInfo.osName, + fakeDatadogContext.deviceInfo.osVersion, + fakeDatadogContext.deviceInfo.osMajorVersion + ) + hasConnectivityInfo(fakeDatadogContext.networkInfo) + hasServiceName(fakeDatadogContext.service) + hasVersion(fakeDatadogContext.version) + hasSampleRate(fakeSampleRate) + } + } + verifyNoMoreInteractions(mockWriter) + assertThat(result).isSameAs(testedScope) + } + + @Test + fun `𝕄 send event 𝕎 handleEvent(AddError) on active view {throwable is ANR}`( + @StringForgery message: String, + @Forgery source: RumErrorSource, + @StringForgery stacktrace: String, + forge: Forge + ) { + // Given + val throwable = ANRException(Thread.currentThread()) + testedScope.activeActionScope = mockActionScope + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + fakeEvent = RumRawEvent.AddError( + message, + source, + throwable, + stacktrace, + isFatal = false, + threads = emptyList(), + attributes = attributes + ) + + // When + val result = testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture()) + + assertThat(firstValue) + .apply { + hasTimestamp(resolveExpectedTimestamp(fakeEvent.eventTime.timestamp)) + hasMessage(message) + hasErrorSource(source) + hasErrorCategory(ErrorEvent.Category.ANR) hasStackTrace(stacktrace) isCrash(false) hasNoThreads() @@ -4596,6 +4675,7 @@ internal class RumViewScopeTest { hasTimestamp(resolveExpectedTimestamp(fakeEvent.eventTime.timestamp)) hasMessage(message) hasErrorSource(source) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasStackTrace(stacktrace) isCrash(false) hasNoThreads() @@ -4670,6 +4750,7 @@ internal class RumViewScopeTest { hasTimestamp(resolveExpectedTimestamp(fakeEvent.eventTime.timestamp)) hasMessage(throwableMessage) hasErrorSource(source) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasStackTrace(stacktrace) isCrash(false) hasNoThreads() @@ -4738,6 +4819,7 @@ internal class RumViewScopeTest { hasMessage(message) hasErrorSource(source) hasStackTrace(stacktrace) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) isCrash(false) hasNoThreads() hasUserInfo(fakeDatadogContext.userInfo) @@ -4794,6 +4876,7 @@ internal class RumViewScopeTest { hasTimestamp(resolveExpectedTimestamp(fakeEvent.eventTime.timestamp)) hasMessage(expectedMessage) hasErrorSource(source) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasStackTrace(throwable.loggableStackTrace()) isCrash(false) hasNoThreads() @@ -4869,6 +4952,7 @@ internal class RumViewScopeTest { hasTimestamp(resolveExpectedTimestamp(fakeEvent.eventTime.timestamp)) hasMessage(message) hasErrorSource(source) + hasErrorCategory(null) hasStackTrace(null) isCrash(false) hasNoThreads() @@ -4955,6 +5039,7 @@ internal class RumViewScopeTest { hasActionId(fakeActionId) hasErrorType(null) hasErrorSourceType(sourceType.toSchemaSourceType()) + hasErrorCategory(null) hasUserSession() hasNoSyntheticsTest() hasLiteSessionPlan() @@ -5036,6 +5121,85 @@ internal class RumViewScopeTest { assertThat(result).isSameAs(testedScope) } + @Test + fun `𝕄 send event 𝕎 handleEvent(AddError) on active view { error fingerprint attribute }`( + @Forgery source: RumErrorSource, + @Forgery throwable: Throwable, + @StringForgery stacktrace: String, + @StringForgery fingerprint: String, + forge: Forge + ) { + // Given + testedScope.activeActionScope = mockActionScope + val mockAttributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + val fullAttributes = mockAttributes.toMutableMap().apply { + put(RumAttributes.ERROR_FINGERPRINT, fingerprint) + } + val throwableMessage = throwable.message + check(!throwableMessage.isNullOrBlank()) { + "Expected throwable to have a non null, non blank message" + } + fakeEvent = RumRawEvent.AddError( + throwableMessage, + source, + throwable, + stacktrace, + isFatal = false, + threads = emptyList(), + attributes = fullAttributes + ) + + // When + val result = testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture()) + + assertThat(firstValue) + .apply { + hasTimestamp(resolveExpectedTimestamp(fakeEvent.eventTime.timestamp)) + hasMessage(throwableMessage) + hasErrorSource(source) + hasStackTrace(stacktrace) + hasErrorFingerprint(fingerprint) + isCrash(false) + hasNoThreads() + hasUserInfo(fakeDatadogContext.userInfo) + hasConnectivityInfo(fakeDatadogContext.networkInfo) + hasView(testedScope.viewId, testedScope.key.name, testedScope.url) + hasApplicationId(fakeParentContext.applicationId) + hasSessionId(fakeParentContext.sessionId) + hasActionId(fakeActionId) + hasLiteSessionPlan() + hasStartReason(fakeParentContext.sessionStartReason) + hasReplay(fakeHasReplay) + containsExactlyContextAttributes(mockAttributes) + hasSource(fakeSourceErrorEvent) + hasUserSession() + hasNoSyntheticsTest() + hasDeviceInfo( + fakeDatadogContext.deviceInfo.deviceName, + fakeDatadogContext.deviceInfo.deviceModel, + fakeDatadogContext.deviceInfo.deviceBrand, + fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), + fakeDatadogContext.deviceInfo.architecture + ) + hasOsInfo( + fakeDatadogContext.deviceInfo.osName, + fakeDatadogContext.deviceInfo.osVersion, + fakeDatadogContext.deviceInfo.osMajorVersion + ) + hasConnectivityInfo(fakeDatadogContext.networkInfo) + hasServiceName(fakeDatadogContext.service) + hasVersion(fakeDatadogContext.version) + hasSampleRate(fakeSampleRate) + } + } + verifyNoMoreInteractions(mockWriter) + assertThat(result).isSameAs(testedScope) + } + @Test fun `𝕄 send event with global attributes 𝕎 handleEvent(AddError)`( @StringForgery message: String, @@ -5072,6 +5236,7 @@ internal class RumViewScopeTest { hasTimestamp(resolveExpectedTimestamp(fakeEvent.eventTime.timestamp)) hasMessage(expectedMessage) hasErrorSource(source) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasStackTrace(throwable.loggableStackTrace()) isCrash(false) hasNoThreads() @@ -5160,6 +5325,7 @@ internal class RumViewScopeTest { hasActionId(fakeActionId) hasErrorType(throwable.javaClass.canonicalName) hasErrorSourceType(sourceType.toSchemaSourceType()) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() hasLiteSessionPlan() @@ -5303,6 +5469,7 @@ internal class RumViewScopeTest { hasActionId(fakeActionId) hasErrorType(throwable.javaClass.canonicalName) hasErrorSourceType(sourceType.toSchemaSourceType()) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() hasLiteSessionPlan() @@ -5428,6 +5595,7 @@ internal class RumViewScopeTest { hasActionId(fakeActionId) hasErrorType(throwable.javaClass.canonicalName) hasErrorSourceType(sourceType.toSchemaSourceType()) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() hasLiteSessionPlan() @@ -5495,6 +5663,7 @@ internal class RumViewScopeTest { hasTimestamp(resolveExpectedTimestamp(fakeEvent.eventTime.timestamp)) hasMessage(expectedMessage) hasErrorSource(source) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasStackTrace(throwable.loggableStackTrace()) isCrash(false) hasNoThreads() @@ -5584,6 +5753,7 @@ internal class RumViewScopeTest { hasActionId(fakeActionId) hasErrorType(throwable.javaClass.canonicalName) hasErrorSourceType(sourceType.toSchemaSourceType()) + hasErrorCategory(ErrorEvent.Category.EXCEPTION) hasUserSession() hasNoSyntheticsTest() hasLiteSessionPlan() @@ -8298,6 +8468,79 @@ internal class RumViewScopeTest { // endregion + @Test + fun `𝕄 produce event safe for serialization 𝕎 handleEvent()`( + forge: Forge + ) { + // Given + val writeWorker = Executors.newCachedThreadPool() + val tasks = mutableListOf>() + whenever(mockRumFeatureScope.withWriteContext(any(), any())) doAnswer { + val callback = it.getArgument<(DatadogContext, EventBatchWriter) -> Unit>(1) + tasks += writeWorker.submit { + callback.invoke(fakeDatadogContext, mockEventBatchWriter) + } + } + whenever(mockWriter.write(eq(mockEventBatchWriter), any())) doAnswer { + when (val event = it.getArgument(1)) { + is ViewEvent -> assertDoesNotThrow { event.toJson() } + is ErrorEvent -> assertDoesNotThrow { event.toJson() } + is ActionEvent -> assertDoesNotThrow { event.toJson() } + is LongTaskEvent -> assertDoesNotThrow { event.toJson() } + // error is on purpose here, because under the hood all the Exceptions are caught + else -> throw Error("unsupported event type ${event::class}") + } + true + } + whenever(rumMonitor.mockInstance.getAttributes()) doReturn forge.exhaustiveAttributes() + + testedScope = RumViewScope( + mockParentScope, + rumMonitor.mockSdkCore, + fakeKey, + fakeEventTime, + fakeAttributes, + mockViewChangedListener, + mockResolver, + mockCpuVitalMonitor, + mockMemoryVitalMonitor, + mockFrameRateVitalMonitor, + featuresContextResolver = mockFeaturesContextResolver, + trackFrustrations = fakeTrackFrustrations, + sampleRate = fakeSampleRate + ) + + // When + repeat(1000) { + testedScope.handleEvent(forge.applicationStartedEvent(), mockWriter) + testedScope.handleEvent( + forge.anyRumEvent( + excluding = listOf( + RumRawEvent.StartView::class.java, + RumRawEvent.StopView::class.java, + RumRawEvent.StartAction::class.java, + RumRawEvent.StopAction::class.java, + RumRawEvent.StartResource::class.java, + RumRawEvent.StopResource::class.java, + RumRawEvent.StopResourceWithError::class.java, + RumRawEvent.StopResourceWithStackTrace::class.java + ) + ), + mockWriter + ) + } + testedScope.handleEvent(forge.stopViewEvent(), mockWriter) + + writeWorker.shutdown() + writeWorker.awaitTermination(5, TimeUnit.SECONDS) + + // Then + tasks.forEach { + // if there is any assertion error, it will be re-thrown + it.get() + } + } + // region Internal private fun mockEvent(): RumRawEvent { 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 5a68fc283e..0f092a6e52 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 @@ -13,6 +13,7 @@ import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.feature.event.ThreadDump import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.internal.utils.loggableStackTrace import com.datadog.android.rum.DdRumContentProvider @@ -23,7 +24,6 @@ import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.RumSessionListener -import com.datadog.android.rum.internal.AppStartTimeProvider import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.internal.RumFeature import com.datadog.android.rum.internal.debug.RumDebugListener @@ -99,7 +99,7 @@ import java.util.concurrent.TimeUnit @ForgeConfiguration(Configurator::class) internal class DatadogRumMonitorTest { - lateinit var testedMonitor: DatadogRumMonitor + private lateinit var testedMonitor: DatadogRumMonitor @Mock lateinit var mockScope: RumScope @@ -134,9 +134,6 @@ internal class DatadogRumMonitorTest { @Mock lateinit var mockInternalLogger: InternalLogger - @Mock - lateinit var mockAppStartTimeProvider: AppStartTimeProvider - @StringForgery(regex = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}") lateinit var fakeApplicationId: String @@ -148,9 +145,6 @@ internal class DatadogRumMonitorTest { @LongForgery(TIMESTAMP_MIN, TIMESTAMP_MAX) var fakeTimestamp: Long = 0L - @LongForgery(min = 0L) - var fakeAppStartTimeNs: Long = 0L - @BoolForgery var fakeBackgroundTrackingEnabled: Boolean = false @@ -160,7 +154,7 @@ internal class DatadogRumMonitorTest { @BeforeEach fun `set up`(forge: Forge) { whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger - whenever(mockAppStartTimeProvider.appStartTimeNs) doReturn fakeAppStartTimeNs + whenever(mockSdkCore.time) doReturn forge.getForgery() fakeAttributes = forge.exhaustiveAttributes() testedMonitor = DatadogRumMonitor( @@ -176,8 +170,7 @@ internal class DatadogRumMonitorTest { mockCpuVitalMonitor, mockMemoryVitalMonitor, mockFrameRateVitalMonitor, - mockSessionListener, - mockAppStartTimeProvider + mockSessionListener ) testedMonitor.rootScope = mockScope } @@ -254,10 +247,7 @@ internal class DatadogRumMonitorTest { } @Test - fun `M send correct sessionId W getCurrentSessionId { session started, sampled in }`( - @StringForgery(type = StringForgeryType.ASCII) key: String, - @StringForgery name: String - ) { + fun `M send correct sessionId W getCurrentSessionId { session started, sampled in }`() { // Given testedMonitor = DatadogRumMonitor( fakeApplicationId, @@ -275,7 +265,7 @@ internal class DatadogRumMonitorTest { mockSessionListener ) val completableFuture = CompletableFuture() - testedMonitor.startView(key, name, fakeAttributes) + testedMonitor.start() Thread.sleep(PROCESSING_DELAY) // When @@ -293,10 +283,7 @@ internal class DatadogRumMonitorTest { } @Test - fun `M send null sessionId W getCurrentSessionId { session started, sampled out }`( - @StringForgery(type = StringForgeryType.ASCII) key: String, - @StringForgery name: String - ) { + fun `M send null sessionId W getCurrentSessionId { session started, sampled out }`() { testedMonitor = DatadogRumMonitor( fakeApplicationId, mockSdkCore, @@ -314,7 +301,7 @@ internal class DatadogRumMonitorTest { ) val completableFuture = CompletableFuture() - testedMonitor.startView(key, name, fakeAttributes) + testedMonitor.start() Thread.sleep(PROCESSING_DELAY) // When @@ -638,7 +625,7 @@ internal class DatadogRumMonitorTest { } @Test - fun `M delegate event to rootScope W onAddErrorWithStacktrace`( + fun `M delegate event to rootScope W addErrorWithStacktrace`( @StringForgery message: String, @Forgery source: RumErrorSource, @StringForgery stacktrace: String @@ -773,7 +760,6 @@ internal class DatadogRumMonitorTest { assertThat(firstValue).isInstanceOf(RumRawEvent.SdkInit::class.java) with(firstValue as RumRawEvent.SdkInit) { assertThat(isAppInForeground).isTrue() - assertThat(appStartTimeNs).isEqualTo(fakeAppStartTimeNs) } } verifyNoMoreInteractions(mockScope, mockWriter) @@ -809,7 +795,6 @@ internal class DatadogRumMonitorTest { assertThat(firstValue).isInstanceOf(RumRawEvent.SdkInit::class.java) with(firstValue as RumRawEvent.SdkInit) { assertThat(isAppInForeground).isFalse() - assertThat(appStartTimeNs).isEqualTo(fakeAppStartTimeNs) } } verifyNoMoreInteractions(mockScope, mockWriter) @@ -974,6 +959,34 @@ internal class DatadogRumMonitorTest { verifyNoMoreInteractions(mockScope, mockWriter) } + @Test + fun `M delegate event to rootScope with all threads W addError`( + @StringForgery message: String, + @Forgery source: RumErrorSource, + @Forgery throwable: Throwable, + @Forgery allThreads: List + ) { + val attributes = fakeAttributes + (RumAttributes.INTERNAL_ALL_THREADS to allThreads) + + testedMonitor.addError(message, source, throwable, attributes) + Thread.sleep(PROCESSING_DELAY) + + argumentCaptor { + verify(mockScope).handleEvent(capture(), same(mockWriter)) + + val event = firstValue as RumRawEvent.AddError + assertThat(event.message).isEqualTo(message) + assertThat(event.source).isEqualTo(source) + assertThat(event.throwable).isEqualTo(throwable) + assertThat(event.stacktrace).isNull() + assertThat(event.isFatal).isFalse + assertThat(event.sourceType).isEqualTo(RumErrorSourceType.ANDROID) + assertThat(event.threads).isEqualTo(allThreads) + assertThat(event.attributes).containsExactlyEntriesOf(fakeAttributes) + } + verifyNoMoreInteractions(mockScope, mockWriter) + } + @Test fun `M delegate event to rootScope with timestamp W addError`( @StringForgery message: String, @@ -1002,7 +1015,7 @@ internal class DatadogRumMonitorTest { } @Test - fun `M delegate event to rootScope with timestamp W onAddErrorWithStacktrace`( + fun `M delegate event to rootScope with timestamp W addErrorWithStacktrace`( @StringForgery message: String, @Forgery source: RumErrorSource, @StringForgery stacktrace: String @@ -1057,7 +1070,7 @@ internal class DatadogRumMonitorTest { } @Test - fun `M delegate event to rootScope W error type onAddErrorWithStacktrace`( + fun `M delegate event to rootScope W error type addErrorWithStacktrace`( @StringForgery message: String, @Forgery source: RumErrorSource, @StringForgery stacktrace: String, @@ -1090,7 +1103,7 @@ internal class DatadogRumMonitorTest { } @RepeatedTest(10) - fun `M delegate event to rootScope W error source type onAddErrorWithStacktrace`( + fun `M delegate event to rootScope W error source type addErrorWithStacktrace`( @StringForgery message: String, @Forgery source: RumErrorSource, @StringForgery stacktrace: String, @@ -1389,7 +1402,6 @@ internal class DatadogRumMonitorTest { mockMemoryVitalMonitor, mockFrameRateVitalMonitor, mockSessionListener, - mockAppStartTimeProvider, mockExecutor ) @@ -1434,7 +1446,6 @@ internal class DatadogRumMonitorTest { mockMemoryVitalMonitor, mockFrameRateVitalMonitor, mockSessionListener, - mockAppStartTimeProvider, mockExecutorService ) @@ -1466,7 +1477,6 @@ internal class DatadogRumMonitorTest { mockMemoryVitalMonitor, mockFrameRateVitalMonitor, mockSessionListener, - mockAppStartTimeProvider, mockExecutorService ) whenever(mockExecutorService.isShutdown).thenReturn(true) @@ -1506,7 +1516,7 @@ internal class DatadogRumMonitorTest { val viewScopes = forge.aList { mock().apply { whenever(getRumContext()) doReturn - RumContext(viewName = forge.aNullable { forge.anAlphaNumericalString() }) + RumContext(viewName = forge.aNullable { forge.anAlphaNumericalString() }) whenever(isActive()) doReturn true } @@ -1542,7 +1552,7 @@ internal class DatadogRumMonitorTest { val viewScopes = forge.aList { mock().apply { whenever(getRumContext()) doReturn - RumContext(viewName = forge.aNullable { forge.anAlphaNumericalString() }) + RumContext(viewName = forge.aNullable { forge.anAlphaNumericalString() }) whenever(isActive()) doReturn false } @@ -1866,7 +1876,7 @@ internal class DatadogRumMonitorTest { if (isMethodOccupied) { throw IllegalStateException( "Only one thread should" + - " be allowed to enter rootScope at the time." + " be allowed to enter rootScope at the time." ) } isMethodOccupied = true diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/ndk/DatadogNdkCrashEventHandlerTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/ndk/DatadogNdkCrashEventHandlerTest.kt deleted file mode 100644 index e20ec4e0a0..0000000000 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/ndk/DatadogNdkCrashEventHandlerTest.kt +++ /dev/null @@ -1,626 +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.ndk - -import com.datadog.android.api.InternalLogger -import com.datadog.android.api.context.DatadogContext -import com.datadog.android.api.context.UserInfo -import com.datadog.android.api.feature.Feature -import com.datadog.android.api.feature.FeatureScope -import com.datadog.android.api.feature.FeatureSdkCore -import com.datadog.android.api.storage.DataWriter -import com.datadog.android.api.storage.EventBatchWriter -import com.datadog.android.core.internal.persistence.Deserializer -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.assertj.ErrorEventAssert -import com.datadog.android.rum.assertj.ViewEventAssert -import com.datadog.android.rum.internal.domain.scope.toErrorSchemaType -import com.datadog.android.rum.model.ErrorEvent -import com.datadog.android.rum.model.ViewEvent -import com.datadog.android.rum.utils.forge.Configurator -import com.datadog.android.rum.utils.verifyLog -import com.google.gson.JsonObject -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.LongForgery -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -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.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.EnumSource -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.doAnswer -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.eq -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoInteractions -import org.mockito.kotlin.whenever -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class DatadogNdkCrashEventHandlerTest { - - private lateinit var testedHandler: NdkCrashEventHandler - - @Mock - lateinit var mockSdkCore: FeatureSdkCore - - @Mock - lateinit var mockRumFeatureScope: FeatureScope - - @Mock - lateinit var mockRumWriter: DataWriter - - @Mock - lateinit var mockRumEventDeserializer: Deserializer - - @Mock - lateinit var mockEventBatchWriter: EventBatchWriter - - @Mock - lateinit var mockInternalLogger: InternalLogger - - @Forgery - lateinit var fakeDatadogContext: DatadogContext - - @BeforeEach - fun `set up`() { - whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mockRumFeatureScope - whenever(mockRumFeatureScope.withWriteContext(any(), any())) doAnswer { - val callback = it.getArgument<(DatadogContext, EventBatchWriter) -> Unit>(1) - callback.invoke(fakeDatadogContext, mockEventBatchWriter) - } - - testedHandler = DatadogNdkCrashEventHandler( - rumEventDeserializer = mockRumEventDeserializer, - internalLogger = mockInternalLogger - ) - } - - @Test - fun `𝕄 send RUM view+error 𝕎 handleEvent()`( - @StringForgery crashMessage: String, - @LongForgery(min = 1) fakeTimestamp: Long, - @StringForgery fakeSignalName: String, - @StringForgery fakeStacktrace: String, - @Forgery viewEvent: ViewEvent, - @Forgery fakeUserInfo: UserInfo, - forge: Forge - ) { - // Given - val fakeServerOffset = - forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) - fakeDatadogContext = fakeDatadogContext.copy( - time = fakeDatadogContext.time.copy( - serverTimeOffsetMs = fakeServerOffset - ) - ) - - val fakeViewEvent = viewEvent.copy( - date = System.currentTimeMillis() - forge.aLong( - min = 0L, - max = DatadogNdkCrashEventHandler.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 - ), - usr = ViewEvent.Usr( - id = fakeUserInfo.id, - name = fakeUserInfo.name, - email = fakeUserInfo.email, - additionalProperties = fakeUserInfo.additionalProperties.toMutableMap() - ) - ) - val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject - - whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) - .doReturn(fakeViewEvent) - - val fakeEvent = mapOf( - "timestamp" to fakeTimestamp, - "signalName" to fakeSignalName, - "stacktrace" to fakeStacktrace, - "message" to crashMessage, - "lastViewEvent" to fakeViewEventJson - ) - - // When - testedHandler.handleEvent(fakeEvent, mockSdkCore, mockRumWriter) - - // Then - argumentCaptor { - verify(mockRumWriter, times(2)).write(eq(mockEventBatchWriter), capture()) - - ErrorEventAssert.assertThat(firstValue as ErrorEvent) - .hasApplicationId(fakeViewEvent.application.id) - .hasSessionId(fakeViewEvent.session.id) - .hasView( - fakeViewEvent.view.id, - fakeViewEvent.view.name, - fakeViewEvent.view.url - ) - .hasMessage(crashMessage) - .hasStackTrace(fakeStacktrace) - .isCrash(true) - .hasErrorSource(RumErrorSource.SOURCE) - .hasErrorSourceType(ErrorEvent.SourceType.NDK) - .hasTimestamp(fakeTimestamp + fakeServerOffset) - .hasUserInfo( - UserInfo( - fakeViewEvent.usr?.id, - fakeViewEvent.usr?.name, - fakeViewEvent.usr?.email, - fakeViewEvent.usr?.additionalProperties.orEmpty() - ) - ) - .hasErrorType(fakeSignalName) - .hasLiteSessionPlan() - .hasDeviceInfo( - fakeDatadogContext.deviceInfo.deviceName, - fakeDatadogContext.deviceInfo.deviceModel, - fakeDatadogContext.deviceInfo.deviceBrand, - fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), - fakeDatadogContext.deviceInfo.architecture - ) - .hasOsInfo( - fakeDatadogContext.deviceInfo.osName, - fakeDatadogContext.deviceInfo.osVersion, - fakeDatadogContext.deviceInfo.osMajorVersion - ) - - ViewEventAssert.assertThat(secondValue as ViewEvent) - .hasVersion(fakeViewEvent.dd.documentVersion + 1) - .hasCrashCount((fakeViewEvent.view.crash?.count ?: 0) + 1) - .isActive(false) - } - } - - @Test - fun `𝕄 send RUM view+error 𝕎 handleEvent() {source_type set}`( - @StringForgery crashMessage: String, - @LongForgery(min = 1) fakeTimestamp: Long, - @StringForgery fakeSignalName: String, - @StringForgery fakeStacktrace: String, - @Forgery viewEvent: ViewEvent, - @Forgery fakeUserInfo: UserInfo, - forge: Forge - ) { - // Given - val fakeServerOffset = - forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) - fakeDatadogContext = fakeDatadogContext.copy( - time = fakeDatadogContext.time.copy( - serverTimeOffsetMs = fakeServerOffset - ) - ) - - val fakeViewEvent = viewEvent.copy( - date = System.currentTimeMillis() - forge.aLong( - min = 0L, - max = DatadogNdkCrashEventHandler.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 - ), - usr = ViewEvent.Usr( - id = fakeUserInfo.id, - name = fakeUserInfo.name, - email = fakeUserInfo.email, - additionalProperties = fakeUserInfo.additionalProperties.toMutableMap() - ) - ) - val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject - - whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) - .doReturn(fakeViewEvent) - - val fakeEvent = mapOf( - "sourceType" to "ndk+il2cpp", - "timestamp" to fakeTimestamp, - "signalName" to fakeSignalName, - "stacktrace" to fakeStacktrace, - "message" to crashMessage, - "lastViewEvent" to fakeViewEventJson - ) - - // When - testedHandler.handleEvent(fakeEvent, mockSdkCore, mockRumWriter) - - // Then - argumentCaptor { - verify(mockRumWriter, times(2)).write(eq(mockEventBatchWriter), capture()) - - ErrorEventAssert.assertThat(firstValue as ErrorEvent) - .hasApplicationId(fakeViewEvent.application.id) - .hasSessionId(fakeViewEvent.session.id) - .hasView( - fakeViewEvent.view.id, - fakeViewEvent.view.name, - fakeViewEvent.view.url - ) - .hasMessage(crashMessage) - .hasStackTrace(fakeStacktrace) - .isCrash(true) - .hasErrorSource(RumErrorSource.SOURCE) - .hasErrorSourceType(ErrorEvent.SourceType.NDK_IL2CPP) - .hasTimestamp(fakeTimestamp + fakeServerOffset) - .hasUserInfo( - UserInfo( - fakeViewEvent.usr?.id, - fakeViewEvent.usr?.name, - fakeViewEvent.usr?.email, - fakeViewEvent.usr?.additionalProperties.orEmpty() - ) - ) - .hasErrorType(fakeSignalName) - .hasLiteSessionPlan() - .hasDeviceInfo( - fakeDatadogContext.deviceInfo.deviceName, - fakeDatadogContext.deviceInfo.deviceModel, - fakeDatadogContext.deviceInfo.deviceBrand, - fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), - fakeDatadogContext.deviceInfo.architecture - ) - .hasOsInfo( - fakeDatadogContext.deviceInfo.osName, - fakeDatadogContext.deviceInfo.osVersion, - fakeDatadogContext.deviceInfo.osMajorVersion - ) - - ViewEventAssert.assertThat(secondValue as ViewEvent) - .hasVersion(fakeViewEvent.dd.documentVersion + 1) - .hasCrashCount((fakeViewEvent.view.crash?.count ?: 0) + 1) - .isActive(false) - } - } - - @Test - fun `𝕄 send RUM view+error 𝕎 handleEvent() {invalid source_type set}`( - @StringForgery crashMessage: String, - @LongForgery(min = 1) fakeTimestamp: Long, - @StringForgery fakeSignalName: String, - @StringForgery fakeStacktrace: String, - @Forgery viewEvent: ViewEvent, - @Forgery fakeUserInfo: UserInfo, - forge: Forge - ) { - // Given - val fakeServerOffset = - forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) - fakeDatadogContext = fakeDatadogContext.copy( - time = fakeDatadogContext.time.copy( - serverTimeOffsetMs = fakeServerOffset - ) - ) - - val fakeViewEvent = viewEvent.copy( - date = System.currentTimeMillis() - forge.aLong( - min = 0L, - max = DatadogNdkCrashEventHandler.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 - ), - usr = ViewEvent.Usr( - id = fakeUserInfo.id, - name = fakeUserInfo.name, - email = fakeUserInfo.email, - additionalProperties = fakeUserInfo.additionalProperties.toMutableMap() - ) - ) - val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject - - whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) - .doReturn(fakeViewEvent) - - val fakeEvent = mapOf( - "sourceType" to "invalid", - "timestamp" to fakeTimestamp, - "signalName" to fakeSignalName, - "stacktrace" to fakeStacktrace, - "message" to crashMessage, - "lastViewEvent" to fakeViewEventJson - ) - - // When - testedHandler.handleEvent(fakeEvent, mockSdkCore, mockRumWriter) - - // Then - argumentCaptor { - verify(mockRumWriter, times(2)).write(eq(mockEventBatchWriter), capture()) - - ErrorEventAssert.assertThat(firstValue as ErrorEvent) - .hasErrorSourceType(ErrorEvent.SourceType.NDK) - } - } - - @Test - fun `𝕄 send RUM view+error 𝕎 handleEvent() {view without usr}`( - @StringForgery crashMessage: String, - @LongForgery(min = 1) fakeTimestamp: Long, - @StringForgery fakeSignalName: String, - @StringForgery fakeStacktrace: String, - @Forgery viewEvent: ViewEvent, - forge: Forge - ) { - // Given - val fakeServerOffset = - forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) - fakeDatadogContext = fakeDatadogContext.copy( - time = fakeDatadogContext.time.copy( - serverTimeOffsetMs = fakeServerOffset - ) - ) - - val fakeViewEvent = viewEvent.copy( - date = System.currentTimeMillis() - forge.aLong( - min = 0L, - max = DatadogNdkCrashEventHandler.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD - 1000 - ), - usr = null - ) - val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject - val fakeEvent = mapOf( - "timestamp" to fakeTimestamp, - "signalName" to fakeSignalName, - "stacktrace" to fakeStacktrace, - "message" to crashMessage, - "lastViewEvent" to fakeViewEventJson - ) - - whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) - .doReturn(fakeViewEvent) - - // When - testedHandler.handleEvent(fakeEvent, mockSdkCore, mockRumWriter) - - // Then - argumentCaptor { - verify(mockRumWriter, times(2)).write(eq(mockEventBatchWriter), capture()) - - ErrorEventAssert.assertThat(firstValue as ErrorEvent) - .hasApplicationId(fakeViewEvent.application.id) - .hasSessionId(fakeViewEvent.session.id) - .hasView( - fakeViewEvent.view.id, - fakeViewEvent.view.name, - fakeViewEvent.view.url - ) - .hasMessage(crashMessage) - .hasStackTrace(fakeStacktrace) - .isCrash(true) - .hasErrorSource(RumErrorSource.SOURCE) - .hasErrorSourceType(ErrorEvent.SourceType.NDK) - .hasTimestamp(fakeTimestamp + fakeServerOffset) - .hasNoUserInfo() - .hasErrorType(fakeSignalName) - .hasLiteSessionPlan() - .hasDeviceInfo( - fakeDatadogContext.deviceInfo.deviceName, - fakeDatadogContext.deviceInfo.deviceModel, - fakeDatadogContext.deviceInfo.deviceBrand, - fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), - fakeDatadogContext.deviceInfo.architecture - ) - .hasOsInfo( - fakeDatadogContext.deviceInfo.osName, - fakeDatadogContext.deviceInfo.osVersion, - fakeDatadogContext.deviceInfo.osMajorVersion - ) - - ViewEventAssert.assertThat(secondValue as ViewEvent) - .hasVersion(fakeViewEvent.dd.documentVersion + 1) - .hasCrashCount((fakeViewEvent.view.crash?.count ?: 0) + 1) - .isActive(false) - } - } - - @Test - fun `𝕄 send only RUM error 𝕎 handleEvent() {view is too old}`( - @StringForgery crashMessage: String, - @LongForgery(min = 1) fakeTimestamp: Long, - @StringForgery fakeSignalName: String, - @StringForgery fakeStacktrace: String, - @Forgery viewEvent: ViewEvent, - @Forgery fakeUserInfo: UserInfo, - forge: Forge - ) { - // Given - val fakeServerOffset = - forge.aLong(min = -fakeTimestamp, max = Long.MAX_VALUE - fakeTimestamp) - fakeDatadogContext = fakeDatadogContext.copy( - time = fakeDatadogContext.time.copy( - serverTimeOffsetMs = fakeServerOffset - ) - ) - val fakeViewEvent = viewEvent.copy( - date = System.currentTimeMillis() - forge.aLong( - min = DatadogNdkCrashEventHandler.VIEW_EVENT_AVAILABILITY_TIME_THRESHOLD + 1 - ), - usr = ViewEvent.Usr( - id = fakeUserInfo.id, - name = fakeUserInfo.name, - email = fakeUserInfo.email, - additionalProperties = fakeUserInfo.additionalProperties.toMutableMap() - ) - ) - val fakeViewEventJson = fakeViewEvent.toJson().asJsonObject - whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) - .doReturn(fakeViewEvent) - val expectedErrorEventSource = with(fakeViewEvent.source) { - if (this != null) { - ErrorEvent.ErrorEventSource.fromJson(this.toJson().asString) - } else { - null - } - } - val fakeEvent = mapOf( - "timestamp" to fakeTimestamp, - "signalName" to fakeSignalName, - "stacktrace" to fakeStacktrace, - "message" to crashMessage, - "lastViewEvent" to fakeViewEventJson - ) - - // When - testedHandler.handleEvent(fakeEvent, mockSdkCore, mockRumWriter) - - // Then - argumentCaptor { - verify(mockRumWriter, times(1)).write(eq(mockEventBatchWriter), capture()) - - ErrorEventAssert.assertThat(firstValue as ErrorEvent) - .hasApplicationId(fakeViewEvent.application.id) - .hasSessionId(fakeViewEvent.session.id) - .hasView( - fakeViewEvent.view.id, - fakeViewEvent.view.name, - fakeViewEvent.view.url - ) - .hasMessage(crashMessage) - .hasStackTrace(fakeStacktrace) - .isCrash(true) - .hasErrorSource(RumErrorSource.SOURCE) - .hasErrorSourceType(ErrorEvent.SourceType.NDK) - .hasTimestamp(fakeTimestamp + fakeServerOffset) - .hasUserInfo( - UserInfo( - fakeViewEvent.usr?.id, - fakeViewEvent.usr?.name, - fakeViewEvent.usr?.email, - fakeViewEvent.usr?.additionalProperties.orEmpty() - ) - ) - .hasErrorType(fakeSignalName) - .hasLiteSessionPlan() - .hasSource(expectedErrorEventSource) - .hasDeviceInfo( - fakeDatadogContext.deviceInfo.deviceName, - fakeDatadogContext.deviceInfo.deviceModel, - fakeDatadogContext.deviceInfo.deviceBrand, - fakeDatadogContext.deviceInfo.deviceType.toErrorSchemaType(), - fakeDatadogContext.deviceInfo.architecture - ) - .hasOsInfo( - fakeDatadogContext.deviceInfo.osName, - fakeDatadogContext.deviceInfo.osVersion, - fakeDatadogContext.deviceInfo.osMajorVersion - ) - } - } - - @Test - fun `𝕄 not send RUM event 𝕎 handleEvent() { RUM feature is not registered }`( - @StringForgery crashMessage: String, - @LongForgery(min = 1) fakeTimestamp: Long, - @StringForgery fakeSignalName: String, - @StringForgery fakeStacktrace: String, - @Forgery viewEvent: ViewEvent - ) { - // Given - val fakeViewEventJson = viewEvent.toJson().asJsonObject - whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null - val fakeEvent = mapOf( - "timestamp" to fakeTimestamp, - "signalName" to fakeSignalName, - "stacktrace" to fakeStacktrace, - "message" to crashMessage, - "lastViewEvent" to fakeViewEventJson - ) - - // When - testedHandler.handleEvent(fakeEvent, mockSdkCore, mockRumWriter) - - // Then - verifyNoInteractions(mockRumWriter, mockEventBatchWriter) - mockInternalLogger.verifyLog( - InternalLogger.Level.INFO, - InternalLogger.Target.USER, - DatadogNdkCrashEventHandler.INFO_RUM_FEATURE_NOT_REGISTERED - ) - } - - @Test - fun `𝕄 not send RUM event 𝕎 handleEvent() { corrupted event, view json deserialization fails }`( - @StringForgery crashMessage: String, - @LongForgery(min = 1) fakeTimestamp: Long, - @StringForgery fakeSignalName: String, - @StringForgery fakeStacktrace: String, - @Forgery fakeViewEventJson: JsonObject - ) { - // Given - val fakeEvent = mutableMapOf( - "timestamp" to fakeTimestamp, - "signalName" to fakeSignalName, - "stacktrace" to fakeStacktrace, - "message" to crashMessage, - "lastViewEvent" to fakeViewEventJson - ) - whenever(mockRumEventDeserializer.deserialize(fakeViewEventJson)) doReturn null - - // When - testedHandler.handleEvent(fakeEvent, mockSdkCore, mockRumWriter) - - // Then - verifyNoInteractions(mockRumWriter, mockEventBatchWriter) - mockInternalLogger.verifyLog( - InternalLogger.Level.WARN, - InternalLogger.Target.USER, - DatadogNdkCrashEventHandler.NDK_CRASH_EVENT_MISSING_MANDATORY_FIELDS - ) - } - - @ParameterizedTest - @EnumSource(ValueMissingType::class) - fun `𝕄 not send RUM event 𝕎 handleEvent() { corrupted event }`( - missingType: ValueMissingType, - @StringForgery crashMessage: String, - @LongForgery(min = 1) fakeTimestamp: Long, - @StringForgery fakeSignalName: String, - @StringForgery fakeStacktrace: String, - @Forgery viewEvent: ViewEvent, - forge: Forge - ) { - // Given - val fakeViewEventJson = viewEvent.toJson().asJsonObject - val fakeEvent = mutableMapOf( - "timestamp" to fakeTimestamp, - "signalName" to fakeSignalName, - "stacktrace" to fakeStacktrace, - "message" to crashMessage, - "lastViewEvent" to fakeViewEventJson - ) - - when (missingType) { - ValueMissingType.MISSING -> fakeEvent.remove(forge.anElementFrom(fakeEvent.keys)) - ValueMissingType.NULL -> fakeEvent[forge.anElementFrom(fakeEvent.keys)] = null - ValueMissingType.WRONG_TYPE -> fakeEvent[forge.anElementFrom(fakeEvent.keys)] = Any() - } - - // When - testedHandler.handleEvent(fakeEvent, mockSdkCore, mockRumWriter) - - // Then - verifyNoInteractions(mockRumWriter, mockEventBatchWriter) - mockInternalLogger.verifyLog( - InternalLogger.Level.WARN, - InternalLogger.Target.USER, - DatadogNdkCrashEventHandler.NDK_CRASH_EVENT_MISSING_MANDATORY_FIELDS - ) - } - - enum class ValueMissingType { - MISSING, - NULL, - WRONG_TYPE - } -} diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacksTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacksTest.kt index e4b4a98ce8..4327535d68 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacksTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/tracking/OreoFragmentLifecycleCallbacksTest.kt @@ -102,7 +102,7 @@ internal class OreoFragmentLifecycleCallbacksTest { whenever(mockActivity.fragmentManager).thenReturn(mockFragmentManager) whenever(mockActivity.window).thenReturn(mockWindow) - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.BASE + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.BASE whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger @@ -312,7 +312,7 @@ internal class OreoFragmentLifecycleCallbacksTest { @Test fun `it will register the callback to fragment manager on O`() { // Given - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.O + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.O // When testedLifecycleCallbacks.register(mockActivity, mockSdkCore) @@ -327,7 +327,7 @@ internal class OreoFragmentLifecycleCallbacksTest { @Test fun `it will unregister the callback from fragment manager on O`() { // Given - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.O + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.O // When testedLifecycleCallbacks.unregister(mockActivity) @@ -339,7 +339,7 @@ internal class OreoFragmentLifecycleCallbacksTest { @Test fun `it will do nothing when calling register on M`() { // Given - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.M + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.M // When testedLifecycleCallbacks.register(mockActivity, mockSdkCore) @@ -351,7 +351,7 @@ internal class OreoFragmentLifecycleCallbacksTest { @Test fun `it will do nothing when calling unregister on M`() { // Given - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.M + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.M // When testedLifecycleCallbacks.unregister(mockActivity) diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/vitals/JankStatsActivityLifecycleListenerTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/vitals/JankStatsActivityLifecycleListenerTest.kt index 9812855925..4ce069e7a6 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/vitals/JankStatsActivityLifecycleListenerTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/vitals/JankStatsActivityLifecycleListenerTest.kt @@ -262,7 +262,7 @@ internal class JankStatsActivityLifecycleListenerTest { // Given whenever(mockDecorView.isHardwareAccelerated) doReturn true val mockBuildSdkVersionProvider: BuildSdkVersionProvider = mock() - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.S + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.S testedJankListener = JankStatsActivityLifecycleListener( mockObserver, mockInternalLogger, @@ -352,7 +352,7 @@ internal class JankStatsActivityLifecycleListenerTest { @BoolForgery isJank: Boolean ) { // Given - val expectedFrameRate = ONE_SECOND_NS.toDouble() / frameDurationNs.toDouble() + val expectedFrameRate = (ONE_SECOND_NS.toDouble() / frameDurationNs.toDouble()).coerceAtMost(MAX_FPS) val frameData = FrameData(timestampNs, frameDurationNs, isJank, emptyList()) // When @@ -376,7 +376,7 @@ internal class JankStatsActivityLifecycleListenerTest { val frameData = FrameData(timestampNs, frameDurationNs, isJank, emptyList()) val mockBuildSdkVersionProvider: BuildSdkVersionProvider = mock() - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.S + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.S val variableRefreshRateListener = JankStatsActivityLifecycleListener( mockObserver, @@ -412,7 +412,7 @@ internal class JankStatsActivityLifecycleListenerTest { val frameData = FrameData(timestampNs, frameDurationNs, isJank, emptyList()) val mockBuildSdkVersionProvider: BuildSdkVersionProvider = mock() - whenever(mockBuildSdkVersionProvider.version()) doReturn Build.VERSION_CODES.R + whenever(mockBuildSdkVersionProvider.version) doReturn Build.VERSION_CODES.R val mockDisplay: Display = mock() whenever(mockDisplay.refreshRate) doReturn displayRefreshRate.toFloat() diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategyTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategyTest.kt index 2e94f7995b..8b752315ae 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategyTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategyTest.kt @@ -17,6 +17,7 @@ import androidx.fragment.app.FragmentManager import com.datadog.android.api.InternalLogger import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.core.internal.system.BuildSdkVersionProvider import com.datadog.android.rum.internal.RumFeature import com.datadog.android.rum.internal.tracking.OreoFragmentLifecycleCallbacks import com.datadog.android.rum.utils.config.GlobalRumMonitorTestConfiguration @@ -24,11 +25,10 @@ import com.datadog.android.rum.utils.forge.Configurator import com.datadog.android.rum.utils.resolveViewUrl import com.datadog.tools.unit.ObjectTest import com.datadog.tools.unit.annotations.TestConfigurationsProvider -import com.datadog.tools.unit.annotations.TestTargetApi -import com.datadog.tools.unit.extensions.ApiLevelExtension import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration import fr.xgouchet.elmyr.Forge +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 @@ -54,7 +54,6 @@ import org.mockito.quality.Strictness @Extensions( ExtendWith(ForgeExtension::class), ExtendWith(MockitoExtension::class), - ExtendWith(ApiLevelExtension::class), ExtendWith(TestConfigurationExtension::class) ) @MockitoSettings(strictness = Strictness.LENIENT) @@ -85,6 +84,9 @@ internal class FragmentViewTrackingStrategyTest : ObjectTest() val mockFragment: android.app.Fragment = mockDeprecatedFragmentWithArguments(forge) @@ -462,9 +484,11 @@ internal class FragmentViewTrackingStrategyTest : ObjectTest() val baseArgumentCaptor = diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/ConfigurationRumForgeryFactory.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/ConfigurationRumForgeryFactory.kt index d50d425aec..0f27042b39 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/ConfigurationRumForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/ConfigurationRumForgeryFactory.kt @@ -44,6 +44,7 @@ internal class ConfigurationRumForgeryFactory : longTaskTrackingStrategy = mock(), backgroundEventTracking = forge.aBool(), trackFrustrations = forge.aBool(), + trackNonFatalAnrs = forge.aBool(), vitalsMonitorUpdateFrequency = forge.aValueFrom(VitalsUpdateFrequency::class.java), sessionListener = mock(), additionalConfig = forge.aMap { aString() to aString() } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/ErrorEventForgeryFactory.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/ErrorEventForgeryFactory.kt index 013ffc8b70..d7c43f0909 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/ErrorEventForgeryFactory.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/ErrorEventForgeryFactory.kt @@ -45,6 +45,7 @@ internal class ErrorEventForgeryFactory : ForgeryFactory { type = forge.aNullable { anAlphabeticalString() }, handling = forge.aNullable { getForgery() }, handlingStack = forge.aNullable { aThrowable().loggableStackTrace() }, + category = forge.aNullable { getForgery() }, threads = forge.aNullable { aList { ErrorEvent.Thread( diff --git a/features/dd-sdk-android-rum/src/test/resources/anr_crash_trace.txt b/features/dd-sdk-android-rum/src/test/resources/anr_crash_trace.txt new file mode 100644 index 0000000000..cab0d90ad6 --- /dev/null +++ b/features/dd-sdk-android-rum/src/test/resources/anr_crash_trace.txt @@ -0,0 +1,1551 @@ +Subject: Input dispatching timed out (f130cca com.datadog.android.sample/com.datadog.android.sample.NavActivity (server) is not responding. Waited 5000ms for MotionEvent) +RssHwmKb: 249844 +RssKb: 211120 +RssAnonKb: 119340 +RssShmemKb: 980 +VmSwapKb: 19256 + + +--- CriticalEventLog --- +capacity: 20 +timestamp_ms: 1707821024474 +window_ms: 300000 + +----- dumping pid: 10348 at 24653821 + +----- pid 10348 at 2024-02-13 11:43:44.465960343+0100 ----- +Cmd line: com.datadog.android.sample +Build fingerprint: 'google/sdk_gphone64_arm64/emu64a:14/UE1A.230829.036.A1/11228894:user/release-keys' +ABI: 'arm64' +Build type: optimized +suspend all histogram: Sum: 242us 99% C.I. 0.096us-59us Avg: 8.344us Max: 59us +DALVIK THREADS (46): +"main" prio=5 tid=1 Runnable + | group="main" sCount=0 ucsCount=0 flags=0 obj=0x73286cd8 self=0xb40000735e697f50 + | sysTid=10348 nice=-10 cgrp=top-app sched=0/0 handle=0x7553fa54f8 + | state=R schedstat=( 7032405053 179598104 2356 ) utm=330 stm=372 core=2 HZ=100 + | stack=0x7fc7ed6000-0x7fc7ed8000 stackSize=8188KB + | held mutexes= "mutator lock"(shared held) + at android.graphics.Paint.getNativeInstance(Paint.java:743) + at android.graphics.BaseRecordingCanvas.drawRect(BaseRecordingCanvas.java:364) + at com.datadog.android.sample.vitals.BadView.onDraw(BadView.kt:72) + at android.view.View.draw(View.java:23889) + at android.view.View.updateDisplayListIfDirty(View.java:22756) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4540) + at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4513) + at android.view.View.updateDisplayListIfDirty(View.java:22712) + at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:694) + at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:700) + at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:798) + at android.view.ViewRootImpl.draw(ViewRootImpl.java:4939) + at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:4643) + at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3822) + at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2465) + at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9305) + at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1339) + at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1348) + at android.view.Choreographer.doCallbacks(Choreographer.java:952) + at android.view.Choreographer.doFrame(Choreographer.java:882) + at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1322) + at android.os.Handler.handleCallback(Handler.java:958) + at android.os.Handler.dispatchMessage(Handler.java:99) + at android.os.Looper.loopOnce(Looper.java:205) + at android.os.Looper.loop(Looper.java:294) + at android.app.ActivityThread.main(ActivityThread.java:8177) + at java.lang.reflect.Method.invoke(Native method) + at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552) + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971) + +"FinalizerDaemon" daemon prio=5 tid=10 Waiting + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x144403a8 self=0xb40000735e6c1ad0 + | sysTid=10360 nice=4 cgrp=top-app sched=0/0 handle=0x721e96ccb0 + | state=S schedstat=( 3002750 1584833 11 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x721e869000-0x721e86b000 stackSize=1039KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x07ba1f11> (a java.lang.Object) + at java.lang.Object.wait(Object.java:386) + at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:210) + - locked <0x07ba1f11> (a java.lang.Object) + at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:231) + at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:309) + at java.lang.Daemons$Daemon.run(Daemons.java:145) + at java.lang.Thread.run(Thread.java:1012) + +"FinalizerWatchdogDaemon" daemon prio=5 tid=11 Sleeping + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x14440420 self=0xb40000735e6c5270 + | sysTid=10361 nice=4 cgrp=top-app sched=0/0 handle=0x721e862cb0 + | state=S schedstat=( 378081 740709 12 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x721e75f000-0x721e761000 stackSize=1039KB + | held mutexes= + at java.lang.Thread.sleep(Native method) + - sleeping on <0x07dc9876> (a java.lang.Object) + at java.lang.Thread.sleep(Thread.java:450) + - locked <0x07dc9876> (a java.lang.Object) + at java.lang.Thread.sleep(Thread.java:355) + at java.lang.Daemons$FinalizerWatchdogDaemon.sleepForNanos(Daemons.java:481) + at java.lang.Daemons$FinalizerWatchdogDaemon.waitForProgress(Daemons.java:544) + at java.lang.Daemons$FinalizerWatchdogDaemon.runInternal(Daemons.java:412) + at java.lang.Daemons$Daemon.run(Daemons.java:145) + at java.lang.Thread.run(Thread.java:1012) + +"ReferenceQueueDaemon" daemon prio=5 tid=13 Waiting + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x14440498 self=0xb40000735e6c6e40 + | sysTid=10359 nice=4 cgrp=top-app sched=0/0 handle=0x721ea76cb0 + | state=S schedstat=( 10028082 254917 20 ) utm=1 stm=0 core=2 HZ=100 + | stack=0x721e973000-0x721e975000 stackSize=1039KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x00a50e77> (a java.lang.Class) + at java.lang.Object.wait(Object.java:386) + at java.lang.Object.wait(Object.java:524) + at java.lang.Daemons$ReferenceQueueDaemon.runInternal(Daemons.java:239) + - locked <0x00a50e77> (a java.lang.Class) + at java.lang.Daemons$Daemon.run(Daemons.java:145) + at java.lang.Thread.run(Thread.java:1012) + +"pool-2-thread-1" prio=5 tid=24 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14441158 self=0xb40000735e6e0f70 + | sysTid=10380 nice=0 cgrp=top-app sched=0/0 handle=0x720cfd0cb0 + | state=S schedstat=( 332059624 5326249 74 ) utm=32 stm=0 core=3 HZ=100 + | stack=0x720cecd000-0x720cecf000 stackSize=1039KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x07b99ae4> (a okhttp3.internal.http2.Http2Stream) + at java.lang.Object.wait(Object.java:386) + at java.lang.Object.wait(Object.java:524) + at okhttp3.internal.http2.Http2Stream.waitForIo$okhttp(Http2Stream.kt:714) + at okhttp3.internal.http2.Http2Stream.takeHeaders(Http2Stream.kt:140) + - locked <0x07b99ae4> (a okhttp3.internal.http2.Http2Stream) + at okhttp3.internal.http2.Http2ExchangeCodec.readResponseHeaders(Http2ExchangeCodec.kt:97) + at okhttp3.internal.connection.Exchange.readResponseHeaders(Exchange.kt:110) + at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.kt:93) + at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) + at com.datadog.android.core.internal.data.upload.CurlInterceptor.intercept(CurlInterceptor.kt:44) + at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) + at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:34) + at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) + at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95) + at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) + at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:83) + at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) + at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76) + at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) + at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201) + at okhttp3.internal.connection.RealCall.execute(RealCall.kt:154) + at com.datadog.android.core.internal.data.upload.DataOkHttpUploader.executeUploadRequest(DataOkHttpUploader.kt:105) + at com.datadog.android.core.internal.data.upload.DataOkHttpUploader.upload(DataOkHttpUploader.kt:54) + at com.datadog.android.core.internal.data.upload.DataUploadRunnable.consumeBatch(DataUploadRunnable.kt:129) + at com.datadog.android.core.internal.data.upload.DataUploadRunnable.handleNextBatch(DataUploadRunnable.kt:88) + at com.datadog.android.core.internal.data.upload.DataUploadRunnable.run(DataUploadRunnable.kt:54) + at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:487) + at java.util.concurrent.FutureTask.run(FutureTask.java:264) + at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"pool-5-thread-1" prio=5 tid=26 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14441408 self=0xb40000735e6df3a0 + | sysTid=10382 nice=0 cgrp=top-app sched=0/0 handle=0x720cdbccb0 + | state=S schedstat=( 19850161 2682874 99 ) utm=1 stm=0 core=0 HZ=100 + | stack=0x720ccb9000-0x720ccbb000 stackSize=1039KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x07909b4d> (a com.datadog.android.rum.internal.anr.ANRDetectorRunnable$CallbackRunnable) + at java.lang.Object.wait(Object.java:386) + at java.lang.Object.wait(Object.java:524) + at com.datadog.android.rum.internal.anr.ANRDetectorRunnable.run(ANRDetectorRunnable.kt:52) + - locked <0x07909b4d> (a com.datadog.android.rum.internal.anr.ANRDetectorRunnable$CallbackRunnable) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"Picasso-refQueue" daemon prio=5 tid=32 TimedWaiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14441c28 self=0xb40000735e6ed220 + | sysTid=10388 nice=10 cgrp=top-app sched=0/0 handle=0x720c661cb0 + | state=S schedstat=( 2393126 1195709 31 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x720c55e000-0x720c560000 stackSize=1039KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x0271c602> (a java.lang.Object) + at java.lang.Object.wait(Object.java:386) + at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:210) + - locked <0x0271c602> (a java.lang.Object) + at com.squareup.picasso.Picasso$CleanupThread.run(Picasso.java:631) + +"Okio Watchdog" daemon prio=5 tid=45 TimedWaiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14442a00 self=0xb40000735e72f300 + | sysTid=10426 nice=0 cgrp=top-app sched=0/0 handle=0x71ef2d1cb0 + | state=S schedstat=( 485499 326083 13 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x71ef1ce000-0x71ef1d0000 stackSize=1039KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x0c521b13> (a java.lang.Class) + at okio.AsyncTimeout$Companion.awaitTimeout$okio(AsyncTimeout.kt:318) + at okio.AsyncTimeout$Watchdog.run(AsyncTimeout.kt:183) + - locked <0x0c521b13> (a java.lang.Class) + +"OkHttp browser-intake-datadoghq.com" daemon prio=5 tid=46 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14442f10 self=0xb40000735e734670 + | sysTid=10430 nice=0 cgrp=top-app sched=0/0 handle=0x71ee1c7cb0 + | state=S schedstat=( 29256791 578875 14 ) utm=2 stm=0 core=2 HZ=100 + | stack=0x71ee0c4000-0x71ee0c6000 stackSize=1039KB + | held mutexes= + native: #00 pc 00062e1c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 0022cfac /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks+140) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #02 pc 0039978c /apex/com.android.art/lib64/libart.so (art::::CheckJNI::SetPrimitiveArrayRegion +1352) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #03 pc 0002de34 /apex/com.android.art/lib64/libopenjdk.so (SocketInputStream_socketRead0+260) (BuildId: fc4c0ac2dde70b1afe348b962a85a634) + native: #04 pc 00377030 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #05 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #06 pc 003ae360 /apex/com.android.art/lib64/libart.so (art::interpreter::ArtInterpreterToCompiledCodeBridge+320) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #07 pc 00398584 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1488) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #08 pc 0050cf2c /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+12964) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #09 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #10 pc 00145c98 /apex/com.android.art/javalib/core-oj.jar (java.net.SocketInputStream.socketRead) + native: #11 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #12 pc 00398d78 /apex/com.android.art/lib64/libart.so (art::interpreter::ArtInterpreterToInterpreterBridge+100) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #13 pc 00398520 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1388) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #14 pc 0050cf2c /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+12964) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #15 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #16 pc 00145b14 /apex/com.android.art/javalib/core-oj.jar (java.net.SocketInputStream.read) + native: #17 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #18 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #19 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #20 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #21 pc 00145aec /apex/com.android.art/javalib/core-oj.jar (java.net.SocketInputStream.read) + native: #22 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #23 pc 0037c560 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+672) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #24 pc 00377168 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #25 pc 0058acb0 /apex/com.android.art/lib64/libart.so (nterp_helper+4016) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #26 pc 0001818e /apex/com.android.conscrypt/javalib/conscrypt.jar (com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.readFromSocket+50) + native: #27 pc 0058ac54 /apex/com.android.art/lib64/libart.so (nterp_helper+3924) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #28 pc 0001800e /apex/com.android.conscrypt/javalib/conscrypt.jar (com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.processDataFromSocket+350) + native: #29 pc 0058ac54 /apex/com.android.art/lib64/libart.so (nterp_helper+3924) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #30 pc 000181d2 /apex/com.android.conscrypt/javalib/conscrypt.jar (com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.readUntilDataAvailable+2) + native: #31 pc 0058ac54 /apex/com.android.art/lib64/libart.so (nterp_helper+3924) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #32 pc 0001812c /apex/com.android.conscrypt/javalib/conscrypt.jar (com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.read+16) + native: #33 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #34 pc 004906b4 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1248) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #35 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #36 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #37 pc 0008a120 (okio.InputStreamSource.read) + native: #38 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #39 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #40 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #41 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #42 pc 0007f0f0 (okio.AsyncTimeout$source$1.read) + native: #43 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #44 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #45 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #46 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #47 pc 0008f1a4 (okio.RealBufferedSource.request) + native: #48 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #49 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #50 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #51 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #52 pc 000903ec (okio.RealBufferedSource.require) + native: #53 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #54 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #55 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #56 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #57 pc 00071904 (okhttp3.internal.http2.Http2Reader.nextFrame) + native: #58 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #59 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #60 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #61 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #62 pc 0006f0a8 (okhttp3.internal.http2.Http2Connection$ReaderRunnable.invoke) + native: #63 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #64 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #65 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #66 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #67 pc 0006eacc (okhttp3.internal.http2.Http2Connection$ReaderRunnable.invoke) + native: #68 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #69 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #70 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #71 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #72 pc 00062510 (okhttp3.internal.concurrent.TaskQueue$execute$1.runOnce) + native: #73 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #74 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #75 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #76 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #77 pc 00063868 (okhttp3.internal.concurrent.TaskRunner.runTask) + native: #78 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #79 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #80 pc 0050a5d4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+2380) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #81 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #82 pc 000634d8 (okhttp3.internal.concurrent.TaskRunner.access$runTask) + native: #83 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #84 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #85 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #86 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #87 pc 00062fd0 (okhttp3.internal.concurrent.TaskRunner$runnable$1.run) + native: #88 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #89 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #90 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #91 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #92 pc 002488d8 /apex/com.android.art/javalib/core-oj.jar (java.util.concurrent.ThreadPoolExecutor.runWorker) + native: #93 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #94 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #95 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #96 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #97 pc 00247774 /apex/com.android.art/javalib/core-oj.jar (java.util.concurrent.ThreadPoolExecutor$Worker.run) + native: #98 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #99 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #100 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #101 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #102 pc 0000308c [anon:dalvik-/apex/com.android.art/javalib/core-oj.jar-transformed] (java.lang.Thread.run) + native: #103 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #104 pc 0037c560 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+672) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #105 pc 00377168 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #106 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #107 pc 0034b8a4 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #108 pc 004f3e30 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback+1888) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #109 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #110 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + at java.net.SocketInputStream.socketRead0(Native method) + at java.net.SocketInputStream.socketRead(SocketInputStream.java:118) + at java.net.SocketInputStream.read(SocketInputStream.java:173) + at java.net.SocketInputStream.read(SocketInputStream.java:143) + at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.readFromSocket(ConscryptEngineSocket.java:983) + at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.processDataFromSocket(ConscryptEngineSocket.java:947) + at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.readUntilDataAvailable(ConscryptEngineSocket.java:862) + at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.read(ConscryptEngineSocket.java:835) + - locked <0x06e78150> (a java.lang.Object) + at okio.InputStreamSource.read(JvmOkio.kt:94) + at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:125) + at okio.RealBufferedSource.request(RealBufferedSource.kt:206) + at okio.RealBufferedSource.require(RealBufferedSource.kt:199) + at okhttp3.internal.http2.Http2Reader.nextFrame(Http2Reader.kt:89) + at okhttp3.internal.http2.Http2Connection$ReaderRunnable.invoke(Http2Connection.kt:618) + at okhttp3.internal.http2.Http2Connection$ReaderRunnable.invoke(Http2Connection.kt:609) + at okhttp3.internal.concurrent.TaskQueue$execute$1.runOnce(TaskQueue.kt:98) + at okhttp3.internal.concurrent.TaskRunner.runTask(TaskRunner.kt:116) + at okhttp3.internal.concurrent.TaskRunner.access$runTask(TaskRunner.kt:42) + at okhttp3.internal.concurrent.TaskRunner$runnable$1.run(TaskRunner.kt:65) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"OkHttp TaskRunner" daemon prio=5 tid=47 TimedWaiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14445c08 self=0xb40000735e732aa0 + | sysTid=10431 nice=0 cgrp=top-app sched=0/0 handle=0x71edbfbcb0 + | state=S schedstat=( 1779620 0 10 ) utm=0 stm=0 core=2 HZ=100 + | stack=0x71edaf8000-0x71edafa000 stackSize=1039KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x0dd89f49> (a okhttp3.internal.concurrent.TaskRunner) + at okhttp3.internal.concurrent.TaskRunner$RealBackend.coordinatorWait(TaskRunner.kt:294) + at okhttp3.internal.concurrent.TaskRunner.awaitTaskToRun(TaskRunner.kt:218) + at okhttp3.internal.concurrent.TaskRunner$runnable$1.run(TaskRunner.kt:59) + - locked <0x0dd89f49> (a okhttp3.internal.concurrent.TaskRunner) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"pool-14-thread-1" prio=5 tid=2 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x13400180 self=0xb40000735e779ef0 + | sysTid=10443 nice=0 cgrp=top-app sched=0/0 handle=0x72746fdcb0 + | state=S schedstat=( 287793 0 2 ) utm=0 stm=0 core=2 HZ=100 + | stack=0x72745fa000-0x72745fc000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506) + at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3466) + at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3437) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623) + at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1176) + at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:905) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"Signal Catcher" daemon prio=10 tid=6 Runnable + | group="system" sCount=0 ucsCount=0 flags=0 obj=0x14440240 self=0xb40000735e699b20 + | sysTid=10354 nice=-20 cgrp=top-app sched=0/0 handle=0x72747fbcb0 + | state=R schedstat=( 659960 1547584 9 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7274704000-0x7274706000 stackSize=991KB + | held mutexes= "mutator lock"(shared held) + native: #00 pc 00438384 /apex/com.android.art/lib64/libart.so (art::DumpNativeStack+108) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #01 pc 00474f5c /apex/com.android.art/lib64/libart.so (art::Thread::DumpStack const+828) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #02 pc 00474624 /apex/com.android.art/lib64/libart.so (art::DumpCheckpoint::Run+208) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #03 pc 0035036c /apex/com.android.art/lib64/libart.so (art::ThreadList::RunCheckpoint+452) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #04 pc 007c6674 /apex/com.android.art/lib64/libart.so (art::ThreadList::Dump+1716) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #05 pc 007c5c30 /apex/com.android.art/lib64/libart.so (art::ThreadList::DumpForSigQuit+744) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #06 pc 00798f1c /apex/com.android.art/lib64/libart.so (art::Runtime::DumpForSigQuit+56) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #07 pc 007a9934 /apex/com.android.art/lib64/libart.so (art::SignalCatcher::HandleSigQuit+1320) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #08 pc 0049e234 /apex/com.android.art/lib64/libart.so (art::SignalCatcher::Run+312) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #09 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #10 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + (no managed stack frames) + +"perfetto_hprof_listener" prio=10 tid=7 Native (still starting up) + | group="" sCount=1 ucsCount=0 flags=1 obj=0x0 self=0xb40000735e692be0 + | sysTid=10355 nice=-20 cgrp=top-app sched=0/0 handle=0x726f6fdcb0 + | state=S schedstat=( 306709 1558083 6 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x726f606000-0x726f608000 stackSize=991KB + | held mutexes= + native: #00 pc 000b7374 /apex/com.android.runtime/lib64/bionic/libc.so (read+4) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 00025360 /apex/com.android.art/lib64/libperfetto_hprof.so (void* std::__1::__thread_proxy >, ArtPlugin_Initialize::$_7> >+316) (BuildId: 45b5dfbcea5fc746bc003aa7e18dbb74) + native: #02 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #03 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + (no managed stack frames) + +"ADB-JDWP Connection Control Thread" daemon prio=0 tid=8 WaitingInMainDebuggerLoop + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x144402b8 self=0xb40000735e6bff00 + | sysTid=10356 nice=-20 cgrp=top-app sched=0/0 handle=0x726f5ffcb0 + | state=S schedstat=( 5197542 5215168 7 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x726f508000-0x726f50a000 stackSize=991KB + | held mutexes= + native: #00 pc 000b8758 /apex/com.android.runtime/lib64/bionic/libc.so (__ppoll+8) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 00072ed0 /apex/com.android.runtime/lib64/bionic/libc.so (poll+92) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #02 pc 0000a75c /apex/com.android.art/lib64/libadbconnection.so (adbconnection::AdbConnectionState::RunPollLoop+724) (BuildId: 18d4f4b1c63b7f9ca4644652b670af77) + native: #03 pc 00008ecc /apex/com.android.art/lib64/libadbconnection.so (adbconnection::CallbackFunction+1456) (BuildId: 18d4f4b1c63b7f9ca4644652b670af77) + native: #04 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #05 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + (no managed stack frames) + +"Jit thread pool worker thread 0" daemon prio=5 tid=9 Native + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x14440330 self=0xb40000735e6c36a0 + | sysTid=10357 nice=9 cgrp=top-app sched=0/0 handle=0x721fc86cb0 + | state=S schedstat=( 27520623 13802000 99 ) utm=1 stm=0 core=3 HZ=100 + | stack=0x721fb87000-0x721fb89000 stackSize=1023KB + | held mutexes= + native: #00 pc 00062e1c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 0022cfac /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks+140) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #02 pc 003f4b1c /apex/com.android.art/lib64/libart.so (art::jit::JitCompileTask::Run+716) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #03 pc 00577524 /apex/com.android.art/lib64/libart.so (art::ThreadPoolWorker::Run+100) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #04 pc 00577424 /apex/com.android.art/lib64/libart.so (art::ThreadPoolWorker::Callback+164) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #05 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #06 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + (no managed stack frames) + +"HeapTaskDaemon" daemon prio=5 tid=12 WaitingForTaskProcessor + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x14445fb0 self=0xb40000735e6be330 + | sysTid=10358 nice=4 cgrp=top-app sched=0/0 handle=0x721fb80cb0 + | state=S schedstat=( 28869749 3710666 31 ) utm=2 stm=0 core=3 HZ=100 + | stack=0x721fa7d000-0x721fa7f000 stackSize=1039KB + | held mutexes= + native: #00 pc 00062e1c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 0022cfac /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks+140) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #02 pc 00393af8 /apex/com.android.art/lib64/libart.so (art::gc::TaskProcessor::GetTask+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #03 pc 00393818 /apex/com.android.art/lib64/libart.so (art::gc::TaskProcessor::RunAllTasks+52) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #04 pc 00377030 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #05 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #06 pc 004906b4 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1248) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #07 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #08 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #09 pc 0002bb10 /apex/com.android.art/javalib/core-libart.jar (java.lang.Daemons$HeapTaskDaemon.runInternal) + native: #10 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #11 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #12 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #13 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #14 pc 0002ae3c /apex/com.android.art/javalib/core-libart.jar (java.lang.Daemons$Daemon.run) + native: #15 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #16 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #17 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #18 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #19 pc 0010ee0c /apex/com.android.art/javalib/core-oj.jar (java.lang.Thread.run) + native: #20 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #21 pc 0037c560 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+672) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #22 pc 00377168 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #23 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #24 pc 0034b8a4 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #25 pc 004f3e30 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback+1888) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #26 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #27 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + at dalvik.system.VMRuntime.runHeapTasks(Native method) + at java.lang.Daemons$HeapTaskDaemon.runInternal(Daemons.java:687) + at java.lang.Daemons$Daemon.run(Daemons.java:145) + at java.lang.Thread.run(Thread.java:1012) + +"binder:10348_1" prio=5 tid=14 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14440510 self=0xb40000735e6cc1b0 + | sysTid=10362 nice=0 cgrp=top-app sched=0/0 handle=0x721a65acb0 + | state=S schedstat=( 231459 1057125 26 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x721a563000-0x721a565000 stackSize=991KB + | held mutexes= + native: #00 pc 000b7698 /apex/com.android.runtime/lib64/bionic/libc.so (__ioctl+8) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 00070668 /apex/com.android.runtime/lib64/bionic/libc.so (ioctl+156) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #02 pc 00096b1c /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool+348) (BuildId: 3efdb374ddb1486eab125c1dab846104) + native: #03 pc 000969ac /system/lib64/libbinder.so (android::PoolThread::threadLoop+24) (BuildId: 3efdb374ddb1486eab125c1dab846104) + native: #04 pc 00010e28 /system/lib64/libutils.so (android::Thread::_threadLoop+584) (BuildId: 4ad4af87e8ab16b872cdbdaf84188131) + native: #05 pc 000edb18 /system/lib64/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell+140) (BuildId: c741d6d101847b558f8cdb0633f23335) + native: #06 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #07 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + (no managed stack frames) + +"binder:10348_2" prio=5 tid=15 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14440588 self=0xb40000735e6ca5e0 + | sysTid=10363 nice=0 cgrp=top-app sched=0/0 handle=0x721955ccb0 + | state=S schedstat=( 43525252 35343205 730 ) utm=2 stm=2 core=1 HZ=100 + | stack=0x7219465000-0x7219467000 stackSize=991KB + | held mutexes= + native: #00 pc 000b7698 /apex/com.android.runtime/lib64/bionic/libc.so (__ioctl+8) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 00070668 /apex/com.android.runtime/lib64/bionic/libc.so (ioctl+156) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #02 pc 00096b1c /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool+348) (BuildId: 3efdb374ddb1486eab125c1dab846104) + native: #03 pc 000969ac /system/lib64/libbinder.so (android::PoolThread::threadLoop+24) (BuildId: 3efdb374ddb1486eab125c1dab846104) + native: #04 pc 00010e28 /system/lib64/libutils.so (android::Thread::_threadLoop+584) (BuildId: 4ad4af87e8ab16b872cdbdaf84188131) + native: #05 pc 000edb18 /system/lib64/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell+140) (BuildId: c741d6d101847b558f8cdb0633f23335) + native: #06 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #07 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + (no managed stack frames) + +"binder:10348_3" prio=5 tid=16 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14440600 self=0xb40000735e6cdd80 + | sysTid=10365 nice=0 cgrp=top-app sched=0/0 handle=0x721845ecb0 + | state=S schedstat=( 31163076 31567625 621 ) utm=1 stm=1 core=0 HZ=100 + | stack=0x7218367000-0x7218369000 stackSize=991KB + | held mutexes= + native: #00 pc 00062e1c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 0022cfac /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks+140) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #02 pc 0040e260 /apex/com.android.art/lib64/libart.so (art::::CheckJNI::CallMethodV +3040) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #03 pc 00526c9c /apex/com.android.art/lib64/libart.so (art::::CheckJNI::CallStaticBooleanMethodV +76) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #04 pc 004df88c /system/lib64/libhwui.so (_JNIEnv::CallStaticBooleanMethod+120) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #05 pc 004df7b0 /system/lib64/libhwui.so (android::HardwareRendererObserver::notify+216) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #06 pc 005f9444 /system/lib64/libhwui.so (android::uirenderer::renderthread::CanvasContext::onSurfaceStatsAvailable+3052) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #07 pc 00027c5c /system/lib64/libandroid.so (std::__1::__function::__func, void >::operator+220) (BuildId: ee15305f0ca03707d21e5e9e9ab687bc) + native: #08 pc 000e67d8 /system/lib64/libgui.so (android::TransactionCompletedListener::onTransactionCompleted+5884) (BuildId: 67cb3d0bd5575f4f3fc95aac325f4723) + native: #09 pc 00128348 /system/lib64/libgui.so (android::BnTransactionCompletedListener::onTransact+1168) (BuildId: 67cb3d0bd5575f4f3fc95aac325f4723) + native: #10 pc 00074580 /system/lib64/libbinder.so (android::BBinder::transact+248) (BuildId: 3efdb374ddb1486eab125c1dab846104) + native: #11 pc 00073100 /system/lib64/libbinder.so (android::IPCThreadState::executeCommand+500) (BuildId: 3efdb374ddb1486eab125c1dab846104) + native: #12 pc 00096c04 /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool+580) (BuildId: 3efdb374ddb1486eab125c1dab846104) + native: #13 pc 000969ac /system/lib64/libbinder.so (android::PoolThread::threadLoop+24) (BuildId: 3efdb374ddb1486eab125c1dab846104) + native: #14 pc 00010e28 /system/lib64/libutils.so (android::Thread::_threadLoop+584) (BuildId: 4ad4af87e8ab16b872cdbdaf84188131) + native: #15 pc 000edb18 /system/lib64/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell+140) (BuildId: c741d6d101847b558f8cdb0633f23335) + native: #16 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #17 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + (no managed stack frames) + +"Profile Saver" daemon prio=5 tid=17 Native + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x14440678 self=0xb40000735e6d4cc0 + | sysTid=10368 nice=9 cgrp=top-app sched=0/0 handle=0x7216ab2cb0 + | state=S schedstat=( 16237665 160667 4 ) utm=1 stm=0 core=3 HZ=100 + | stack=0x72169bb000-0x72169bd000 stackSize=991KB + | held mutexes= + native: #00 pc 00062e20 /apex/com.android.runtime/lib64/bionic/libc.so (syscall+32) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 002bba38 /apex/com.android.art/lib64/libart.so (art::ConditionVariable::TimedWait+252) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #02 pc 00400bb0 /apex/com.android.art/lib64/libart.so (art::ProfileSaver::Run+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #03 pc 003fd0b4 /apex/com.android.art/lib64/libart.so (art::ProfileSaver::RunProfileSaverThread+152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #04 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #05 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + (no managed stack frames) + +"WM.task-1" prio=5 tid=18 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x144406f0 self=0xb40000735e6c8a10 + | sysTid=10369 nice=0 cgrp=top-app sched=0/0 handle=0x721583ecb0 + | state=S schedstat=( 18512332 3658292 71 ) utm=1 stm=0 core=3 HZ=100 + | stack=0x721573b000-0x721573d000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506) + at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3466) + at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3437) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623) + at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:435) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"StethoListener-main" prio=5 tid=19 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14440860 self=0xb40000735e6d30f0 + | sysTid=10370 nice=0 cgrp=top-app sched=0/0 handle=0x7215734cb0 + | state=S schedstat=( 590831 0 1 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x7215631000-0x7215633000 stackSize=1039KB + | held mutexes= + native: #00 pc 000b8174 /apex/com.android.runtime/lib64/bionic/libc.so (__accept4+4) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 0001168c /system/lib64/libnetd_client.so ((anonymous namespace)::netdClientAccept4 +72) (BuildId: 4164d0544a756c9eb008bd07dc00f744) + native: #02 pc 000665a4 /apex/com.android.runtime/lib64/bionic/libc.so (accept4+44) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #03 pc 000247ec /apex/com.android.art/lib64/libjavacore.so (Linux_accept+172) (BuildId: 058a3af6bcbdbcdce4bfe922408e624f) + native: #04 pc 00377030 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #05 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #06 pc 004906b4 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1248) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #07 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #08 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #09 pc 000376ac /apex/com.android.art/javalib/core-libart.jar (libcore.io.ForwardingOs.accept) + native: #10 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #11 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #12 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #13 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #14 pc 00036390 /apex/com.android.art/javalib/core-libart.jar (libcore.io.BlockGuardOs.accept) + native: #15 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #16 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #17 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #18 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #19 pc 000376ac /apex/com.android.art/javalib/core-libart.jar (libcore.io.ForwardingOs.accept) + native: #20 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #21 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #22 pc 0050aca4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+4124) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #23 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #24 pc 000200d8 /apex/com.android.art/javalib/core-libart.jar (android.system.Os.accept) + native: #25 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #26 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #27 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #28 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #29 pc 000200bc /apex/com.android.art/javalib/core-libart.jar (android.system.Os.accept) + native: #30 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #31 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #32 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #33 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #34 pc 0049c340 /system/framework/framework.jar (android.net.LocalSocketImpl.accept) + native: #35 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #36 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #37 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #38 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #39 pc 0049b960 /system/framework/framework.jar (android.net.LocalServerSocket.accept) + native: #40 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #41 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #42 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #43 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #44 pc 00fd3b10 (com.facebook.stetho.server.LocalSocketServer.listenOnAddress) + native: #45 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #46 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #47 pc 0050a5d4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+2380) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #48 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #49 pc 00fd3c50 (com.facebook.stetho.server.LocalSocketServer.run) + native: #50 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #51 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #52 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #53 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #54 pc 00fd407c (com.facebook.stetho.server.ServerManager$1.run) + native: #55 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #56 pc 0037c560 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+672) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #57 pc 00377168 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #58 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #59 pc 0034b8a4 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #60 pc 004f3e30 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback+1888) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #61 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #62 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + at libcore.io.Linux.accept(Native method) + at libcore.io.ForwardingOs.accept(ForwardingOs.java:91) + at libcore.io.BlockGuardOs.accept(BlockGuardOs.java:66) + at libcore.io.ForwardingOs.accept(ForwardingOs.java:91) + at android.system.Os.accept(Os.java:57) + at android.system.Os.accept(Os.java:51) + at android.net.LocalSocketImpl.accept(LocalSocketImpl.java:303) + at android.net.LocalServerSocket.accept(LocalServerSocket.java:93) + at com.facebook.stetho.server.LocalSocketServer.listenOnAddress(LocalSocketServer.java:83) + at com.facebook.stetho.server.LocalSocketServer.run(LocalSocketServer.java:72) + at com.facebook.stetho.server.ServerManager$1.run(ServerManager.java:38) + +"pool-3-thread-1" prio=5 tid=20 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14440ce0 self=0xb40000735e6cf950 + | sysTid=10372 nice=0 cgrp=top-app sched=0/0 handle=0x721562acb0 + | state=S schedstat=( 229639950 18737958 144 ) utm=22 stm=0 core=1 HZ=100 + | stack=0x7215527000-0x7215529000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506) + at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3466) + at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3437) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623) + at java.util.concurrent.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:485) + at java.util.concurrent.LinkedBlockingDeque.take(LinkedBlockingDeque.java:673) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"WM.task-2" prio=5 tid=21 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14440fb0 self=0xb40000735e6d8460 + | sysTid=10378 nice=0 cgrp=top-app sched=0/0 handle=0x7215520cb0 + | state=S schedstat=( 1378207 69208 6 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x721541d000-0x721541f000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506) + at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3466) + at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3437) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623) + at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:435) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"kronos-android" prio=5 tid=22 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14440e40 self=0xb40000735e6da030 + | sysTid=10374 nice=0 cgrp=top-app sched=0/0 handle=0x720c0dacb0 + | state=S schedstat=( 1912543 16205166 15 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x720bfd7000-0x720bfd9000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506) + at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3466) + at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3437) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623) + at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:435) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"ConnectivityThread" prio=5 tid=23 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14441078 self=0xb40000735e6d6890 + | sysTid=10379 nice=0 cgrp=top-app sched=0/0 handle=0x720d0dacb0 + | state=S schedstat=( 6605873 4697374 18 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x720cfd7000-0x720cfd9000 stackSize=1039KB + | held mutexes= + native: #00 pc 000b8658 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 000101d8 /system/lib64/libutils.so (android::Looper::pollOnce+204) (BuildId: 4ad4af87e8ab16b872cdbdaf84188131) + native: #02 pc 00183604 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce+44) (BuildId: c741d6d101847b558f8cdb0633f23335) + native: #03 pc 00377030 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #04 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #05 pc 004906b4 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1248) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #06 pc 0050a5d4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+2380) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #07 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #08 pc 001fce24 /system/framework/framework.jar (android.os.MessageQueue.next) + native: #09 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #10 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #11 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #12 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #13 pc 001fbe08 /system/framework/framework.jar (android.os.Looper.loopOnce) + native: #14 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #15 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #16 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #17 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #18 pc 001fc57c /system/framework/framework.jar (android.os.Looper.loop) + native: #19 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #20 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #21 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #22 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #23 pc 001d603c /system/framework/framework.jar (android.os.HandlerThread.run) + native: #24 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #25 pc 0037c560 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+672) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #26 pc 00377168 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #27 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #28 pc 0034b8a4 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #29 pc 004f3e30 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback+1888) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #30 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #31 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + at android.os.MessageQueue.nativePollOnce(Native method) + at android.os.MessageQueue.next(MessageQueue.java:335) + at android.os.Looper.loopOnce(Looper.java:162) + at android.os.Looper.loop(Looper.java:294) + at android.os.HandlerThread.run(HandlerThread.java:67) + +"pool-4-thread-1" prio=5 tid=25 TimedWaiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x144412b0 self=0xb40000735e6dd7d0 + | sysTid=10381 nice=0 cgrp=top-app sched=0/0 handle=0x720cec6cb0 + | state=S schedstat=( 372866928 12209419 127 ) utm=30 stm=6 core=0 HZ=100 + | stack=0x720cdc3000-0x720cdc5000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:252) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1672) + at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1188) + at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:905) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"pool-6-thread-1" prio=5 tid=27 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14441640 self=0xb40000735e6e4710 + | sysTid=10383 nice=0 cgrp=top-app sched=0/0 handle=0x720ccb2cb0 + | state=S schedstat=( 15303209 631542 25 ) utm=1 stm=0 core=3 HZ=100 + | stack=0x720cbaf000-0x720cbb1000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506) + at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3466) + at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3437) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623) + at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:435) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"queued-work-looper" prio=5 tid=28 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x144417b0 self=0xb40000735e6e2b40 + | sysTid=10384 nice=-2 cgrp=top-app sched=0/0 handle=0x720ca89cb0 + | state=S schedstat=( 2264584 425458 12 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x720c986000-0x720c988000 stackSize=1039KB + | held mutexes= + native: #00 pc 000b8658 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 000101d8 /system/lib64/libutils.so (android::Looper::pollOnce+204) (BuildId: 4ad4af87e8ab16b872cdbdaf84188131) + native: #02 pc 00183604 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce+44) (BuildId: c741d6d101847b558f8cdb0633f23335) + native: #03 pc 00377030 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #04 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #05 pc 004906b4 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1248) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #06 pc 0050a5d4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+2380) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #07 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #08 pc 001fce24 /system/framework/framework.jar (android.os.MessageQueue.next) + native: #09 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #10 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #11 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #12 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #13 pc 001fbe08 /system/framework/framework.jar (android.os.Looper.loopOnce) + native: #14 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #15 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #16 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #17 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #18 pc 001fc57c /system/framework/framework.jar (android.os.Looper.loop) + native: #19 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #20 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #21 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #22 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #23 pc 001d603c /system/framework/framework.jar (android.os.HandlerThread.run) + native: #24 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #25 pc 0037c560 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+672) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #26 pc 00377168 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #27 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #28 pc 0034b8a4 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #29 pc 004f3e30 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback+1888) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #30 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #31 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + at android.os.MessageQueue.nativePollOnce(Native method) + at android.os.MessageQueue.next(MessageQueue.java:335) + at android.os.Looper.loopOnce(Looper.java:162) + at android.os.Looper.loop(Looper.java:294) + at android.os.HandlerThread.run(HandlerThread.java:67) + +"dd-task-scheduler" daemon prio=5 tid=29 TimedWaiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14441890 self=0xb40000735e6e9a80 + | sysTid=10385 nice=0 cgrp=top-app sched=0/0 handle=0x720c97fcb0 + | state=S schedstat=( 11528039 1260626 33 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x720c87c000-0x720c87e000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:252) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1672) + at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1188) + at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:905) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"Picasso-Stats" prio=5 tid=30 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x144419e8 self=0xb40000735e6e7eb0 + | sysTid=10386 nice=10 cgrp=top-app sched=0/0 handle=0x720c875cb0 + | state=S schedstat=( 3405417 2243503 33 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x720c772000-0x720c774000 stackSize=1039KB + | held mutexes= + native: #00 pc 000b8658 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 000101d8 /system/lib64/libutils.so (android::Looper::pollOnce+204) (BuildId: 4ad4af87e8ab16b872cdbdaf84188131) + native: #02 pc 00183604 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce+44) (BuildId: c741d6d101847b558f8cdb0633f23335) + native: #03 pc 00377030 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #04 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #05 pc 004906b4 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1248) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #06 pc 0050a5d4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+2380) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #07 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #08 pc 001fce24 /system/framework/framework.jar (android.os.MessageQueue.next) + native: #09 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #10 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #11 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #12 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #13 pc 001fbe08 /system/framework/framework.jar (android.os.Looper.loopOnce) + native: #14 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #15 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #16 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #17 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #18 pc 001fc57c /system/framework/framework.jar (android.os.Looper.loop) + native: #19 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #20 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #21 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #22 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #23 pc 001d603c /system/framework/framework.jar (android.os.HandlerThread.run) + native: #24 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #25 pc 0037c560 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+672) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #26 pc 00377168 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #27 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #28 pc 0034b8a4 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #29 pc 004f3e30 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback+1888) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #30 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #31 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + at android.os.MessageQueue.nativePollOnce(Native method) + at android.os.MessageQueue.next(MessageQueue.java:335) + at android.os.Looper.loopOnce(Looper.java:162) + at android.os.Looper.loop(Looper.java:294) + at android.os.HandlerThread.run(HandlerThread.java:67) + +"Picasso-Dispatcher" prio=5 tid=31 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14441b08 self=0xb40000735e6e62e0 + | sysTid=10387 nice=10 cgrp=top-app sched=0/0 handle=0x720c76bcb0 + | state=S schedstat=( 5238751 2717625 34 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x720c668000-0x720c66a000 stackSize=1039KB + | held mutexes= + native: #00 pc 000b8658 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 000101d8 /system/lib64/libutils.so (android::Looper::pollOnce+204) (BuildId: 4ad4af87e8ab16b872cdbdaf84188131) + native: #02 pc 00183604 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce+44) (BuildId: c741d6d101847b558f8cdb0633f23335) + native: #03 pc 00377030 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #04 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #05 pc 004906b4 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1248) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #06 pc 0050a5d4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+2380) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #07 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #08 pc 001fce24 /system/framework/framework.jar (android.os.MessageQueue.next) + native: #09 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #10 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #11 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #12 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #13 pc 001fbe08 /system/framework/framework.jar (android.os.Looper.loopOnce) + native: #14 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #15 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #16 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #17 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #18 pc 001fc57c /system/framework/framework.jar (android.os.Looper.loop) + native: #19 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #20 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #21 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #22 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #23 pc 001d603c /system/framework/framework.jar (android.os.HandlerThread.run) + native: #24 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #25 pc 0037c560 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+672) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #26 pc 00377168 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #27 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #28 pc 0034b8a4 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #29 pc 004f3e30 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback+1888) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #30 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #31 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + at android.os.MessageQueue.nativePollOnce(Native method) + at android.os.MessageQueue.next(MessageQueue.java:335) + at android.os.Looper.loopOnce(Looper.java:162) + at android.os.Looper.loop(Looper.java:294) + at android.os.HandlerThread.run(HandlerThread.java:67) + +"pool-12-thread-1" prio=5 tid=33 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14441eb0 self=0xb40000735e6eedf0 + | sysTid=10389 nice=0 cgrp=top-app sched=0/0 handle=0x720c557cb0 + | state=S schedstat=( 6886462 2484623 12 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x720c454000-0x720c456000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506) + at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3466) + at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3437) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623) + at java.util.concurrent.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:485) + at java.util.concurrent.LinkedBlockingDeque.take(LinkedBlockingDeque.java:673) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"kronos-android" prio=5 tid=34 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14442010 self=0xb40000735e6f09c0 + | sysTid=10390 nice=0 cgrp=top-app sched=0/0 handle=0x720c44dcb0 + | state=S schedstat=( 1726499 1478293 8 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x720c34a000-0x720c34c000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506) + at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3466) + at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3437) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623) + at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:435) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"pool-11-thread-1" prio=5 tid=35 TimedWaiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14442180 self=0xb40000735e6eb650 + | sysTid=10391 nice=0 cgrp=top-app sched=0/0 handle=0x720c343cb0 + | state=S schedstat=( 13715793 2833625 15 ) utm=1 stm=0 core=1 HZ=100 + | stack=0x720c240000-0x720c242000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:252) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1672) + at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1188) + at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:905) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"RenderThread" daemon prio=7 tid=36 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x144422d8 self=0xb40000735e6f5d30 + | sysTid=10392 nice=-10 cgrp=top-app sched=0/0 handle=0x720c239cb0 + | state=S schedstat=( 824844538 44759803 1293 ) utm=35 stm=47 core=0 HZ=100 + | stack=0x720c142000-0x720c144000 stackSize=991KB + | held mutexes= + native: #00 pc 00062e1c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 000677c8 /apex/com.android.runtime/lib64/bionic/libc.so (__futex_wait_ex+144) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #02 pc 000cc8f8 /apex/com.android.runtime/lib64/bionic/libc.so (NonPI::MutexLockWithTimeout+344) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #03 pc 00050094 /system/lib64/libc++.so (std::__1::mutex::lock+8) (BuildId: 12d4c26c3edc0fb330898d775b3910f5) + native: #04 pc 00202a54 /system/lib64/libhwui.so (android::uirenderer::renderthread::CanvasContext::draw+676) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #05 pc 003e9bd0 /system/lib64/libhwui.so (android::uirenderer::renderthread::CanvasContext::prepareAndDraw+352) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #06 pc 0057808c /system/lib64/libhwui.so (std::__1::__function::__func, void >::operator +164) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #07 pc 002aab44 /system/lib64/libhwui.so (android::uirenderer::renderthread::RenderThread::threadLoop+716) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #08 pc 00010e28 /system/lib64/libutils.so (android::Thread::_threadLoop+584) (BuildId: 4ad4af87e8ab16b872cdbdaf84188131) + native: #09 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #10 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + (no managed stack frames) + +"WM.task-3" prio=5 tid=37 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14442350 self=0xb40000735e6f2590 + | sysTid=10395 nice=0 cgrp=top-app sched=0/0 handle=0x71fbfd0cb0 + | state=S schedstat=( 6513959 93000 5 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x71fbecd000-0x71fbecf000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506) + at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3466) + at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3437) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623) + at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:435) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"FrameMetricsAggregator" prio=5 tid=38 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14442418 self=0xb40000735e6fcc70 + | sysTid=10396 nice=0 cgrp=top-app sched=0/0 handle=0x71f86fbcb0 + | state=S schedstat=( 53389021 13510080 486 ) utm=5 stm=0 core=0 HZ=100 + | stack=0x71f85f8000-0x71f85fa000 stackSize=1039KB + | held mutexes= + native: #00 pc 000b8658 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 000101d8 /system/lib64/libutils.so (android::Looper::pollOnce+204) (BuildId: 4ad4af87e8ab16b872cdbdaf84188131) + native: #02 pc 00183604 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce+44) (BuildId: c741d6d101847b558f8cdb0633f23335) + native: #03 pc 00377030 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #04 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #05 pc 004906b4 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1248) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #06 pc 0050a5d4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+2380) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #07 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #08 pc 001fce24 /system/framework/framework.jar (android.os.MessageQueue.next) + native: #09 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #10 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #11 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #12 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #13 pc 001fbe08 /system/framework/framework.jar (android.os.Looper.loopOnce) + native: #14 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #15 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #16 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #17 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #18 pc 001fc57c /system/framework/framework.jar (android.os.Looper.loop) + native: #19 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #20 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #21 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #22 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #23 pc 001d603c /system/framework/framework.jar (android.os.HandlerThread.run) + native: #24 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #25 pc 0037c560 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+672) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #26 pc 00377168 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #27 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #28 pc 0034b8a4 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #29 pc 004f3e30 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback+1888) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #30 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #31 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + at android.os.MessageQueue.nativePollOnce(Native method) + at android.os.MessageQueue.next(MessageQueue.java:335) + at android.os.Looper.loopOnce(Looper.java:162) + at android.os.Looper.loop(Looper.java:294) + at android.os.HandlerThread.run(HandlerThread.java:67) + +"SurfaceSyncGroupTimer" prio=5 tid=39 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x144424f8 self=0xb40000735e701fe0 + | sysTid=10397 nice=0 cgrp=top-app sched=0/0 handle=0x71f75f1cb0 + | state=S schedstat=( 231958 0 3 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x71f74ee000-0x71f74f0000 stackSize=1039KB + | held mutexes= + native: #00 pc 000b8658 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 000101d8 /system/lib64/libutils.so (android::Looper::pollOnce+204) (BuildId: 4ad4af87e8ab16b872cdbdaf84188131) + native: #02 pc 00183604 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce+44) (BuildId: c741d6d101847b558f8cdb0633f23335) + native: #03 pc 00377030 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #04 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #05 pc 004906b4 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+1248) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #06 pc 0050a5d4 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+2380) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #07 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #08 pc 001fce24 /system/framework/framework.jar (android.os.MessageQueue.next) + native: #09 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #10 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #11 pc 00509f94 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+780) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #12 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #13 pc 001fbe08 /system/framework/framework.jar (android.os.Looper.loopOnce) + native: #14 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #15 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #16 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #17 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #18 pc 001fc57c /system/framework/framework.jar (android.os.Looper.loop) + native: #19 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #20 pc 0049120c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall+4152) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #21 pc 0050a2f8 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp+1648) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #22 pc 003797d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #23 pc 001d603c /system/framework/framework.jar (android.os.HandlerThread.run) + native: #24 pc 0037cde0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute +356) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #25 pc 0037c560 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+672) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #26 pc 00377168 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #27 pc 003605a4 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #28 pc 0034b8a4 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke+144) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #29 pc 004f3e30 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback+1888) (BuildId: b10f5696fea1b32039b162aef3850ed3) + native: #30 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #31 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + at android.os.MessageQueue.nativePollOnce(Native method) + at android.os.MessageQueue.next(MessageQueue.java:335) + at android.os.Looper.loopOnce(Looper.java:162) + at android.os.Looper.loop(Looper.java:294) + at android.os.HandlerThread.run(HandlerThread.java:67) + +"hwuiTask0" daemon prio=6 tid=40 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x144425d8 self=0xb40000735e70aaf0 + | sysTid=10399 nice=-2 cgrp=top-app sched=0/0 handle=0x71f73e9cb0 + | state=S schedstat=( 171873 18000 3 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x71f72f2000-0x71f72f4000 stackSize=991KB + | held mutexes= + native: #00 pc 00062e1c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 000677c8 /apex/com.android.runtime/lib64/bionic/libc.so (__futex_wait_ex+144) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #02 pc 000ca970 /apex/com.android.runtime/lib64/bionic/libc.so (pthread_cond_wait+76) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #03 pc 00052fd0 /system/lib64/libc++.so (std::__1::condition_variable::wait+20) (BuildId: 12d4c26c3edc0fb330898d775b3910f5) + native: #04 pc 0049650c /system/lib64/libhwui.so (android::uirenderer::CommonPool::workerLoop+108) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #05 pc 004963f8 /system/lib64/libhwui.so (void std::__1::__thread_execute >, android::uirenderer::CommonPool::CommonPool::$_0> +184) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #06 pc 0049633c /system/lib64/libhwui.so (void* std::__1::__thread_proxy >, android::uirenderer::CommonPool::CommonPool::$_0> > +40) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #07 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #08 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + (no managed stack frames) + +"hwuiTask1" daemon prio=6 tid=41 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14442650 self=0xb40000735e708f20 + | sysTid=10400 nice=-2 cgrp=top-app sched=0/0 handle=0x71f5d6bcb0 + | state=S schedstat=( 128958 0 2 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x71f5c74000-0x71f5c76000 stackSize=991KB + | held mutexes= + native: #00 pc 00062e1c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 000677c8 /apex/com.android.runtime/lib64/bionic/libc.so (__futex_wait_ex+144) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #02 pc 000ca970 /apex/com.android.runtime/lib64/bionic/libc.so (pthread_cond_wait+76) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #03 pc 00052fd0 /system/lib64/libc++.so (std::__1::condition_variable::wait+20) (BuildId: 12d4c26c3edc0fb330898d775b3910f5) + native: #04 pc 0049650c /system/lib64/libhwui.so (android::uirenderer::CommonPool::workerLoop+108) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #05 pc 004963f8 /system/lib64/libhwui.so (void std::__1::__thread_execute >, android::uirenderer::CommonPool::CommonPool::$_0> +184) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #06 pc 0049633c /system/lib64/libhwui.so (void* std::__1::__thread_proxy >, android::uirenderer::CommonPool::CommonPool::$_0> > +40) (BuildId: 1178351e2ee9668d0f2e2865813d9ee6) + native: #07 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #08 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + (no managed stack frames) + +"binder:10348_4" prio=5 tid=42 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x144426c8 self=0xb40000735e718970 + | sysTid=10401 nice=0 cgrp=top-app sched=0/0 handle=0x71f5c6dcb0 + | state=S schedstat=( 191376 0 4 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x71f5b76000-0x71f5b78000 stackSize=991KB + | held mutexes= + native: #00 pc 000b7698 /apex/com.android.runtime/lib64/bionic/libc.so (__ioctl+8) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 00070668 /apex/com.android.runtime/lib64/bionic/libc.so (ioctl+156) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #02 pc 00096b1c /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool+348) (BuildId: 3efdb374ddb1486eab125c1dab846104) + native: #03 pc 000969ac /system/lib64/libbinder.so (android::PoolThread::threadLoop+24) (BuildId: 3efdb374ddb1486eab125c1dab846104) + native: #04 pc 00010e28 /system/lib64/libutils.so (android::Thread::_threadLoop+584) (BuildId: 4ad4af87e8ab16b872cdbdaf84188131) + native: #05 pc 000edb18 /system/lib64/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell+140) (BuildId: c741d6d101847b558f8cdb0633f23335) + native: #06 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #07 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + (no managed stack frames) + +"pool-8-thread-1" prio=5 tid=43 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14442740 self=0xb40000735e71dce0 + | sysTid=10404 nice=0 cgrp=top-app sched=0/0 handle=0x71f14e5cb0 + | state=S schedstat=( 274001799 23559289 205 ) utm=27 stm=0 core=0 HZ=100 + | stack=0x71f13e2000-0x71f13e4000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506) + at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3466) + at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3437) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623) + at java.util.concurrent.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:485) + at java.util.concurrent.LinkedBlockingDeque.take(LinkedBlockingDeque.java:673) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"pool-7-thread-1" prio=5 tid=44 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x144428a0 self=0xb40000735e723050 + | sysTid=10405 nice=0 cgrp=top-app sched=0/0 handle=0x71f03dbcb0 + | state=S schedstat=( 104415831 8148874 206 ) utm=10 stm=0 core=3 HZ=100 + | stack=0x71f02d8000-0x71f02da000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506) + at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3466) + at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3437) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623) + at java.util.concurrent.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:485) + at java.util.concurrent.LinkedBlockingDeque.take(LinkedBlockingDeque.java:673) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"OkHttp TaskRunner" daemon prio=5 tid=48 TimedWaiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14445df8 self=0xb40000735e737e10 + | sysTid=10432 nice=0 cgrp=top-app sched=0/0 handle=0x71eb5fbcb0 + | state=S schedstat=( 727001 0 2 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x71eb4f8000-0x71eb4fa000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:252) + at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:401) + at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:903) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1070) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"OkHttp TaskRunner" daemon prio=5 tid=49 TimedWaiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x14445ee8 self=0xb40000735e736240 + | sysTid=10433 nice=0 cgrp=top-app sched=0/0 handle=0x71ea4f1cb0 + | state=S schedstat=( 117250 299500 1 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x71ea3ee000-0x71ea3f0000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:252) + at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:401) + at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:903) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1070) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) + at java.lang.Thread.run(Thread.java:1012) + +"binder:10348_3" prio=5 (not attached) + | sysTid=10402 nice=0 cgrp=top-app + | state=S schedstat=( 1133847 9776449 480 ) utm=0 stm=0 core=1 HZ=100 + native: #00 pc 00062e1c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #01 pc 000677c8 /apex/com.android.runtime/lib64/bionic/libc.so (__futex_wait_ex+144) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #02 pc 000ca970 /apex/com.android.runtime/lib64/bionic/libc.so (pthread_cond_wait+76) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #03 pc 00052fd0 /system/lib64/libc++.so (std::__1::condition_variable::wait+20) (BuildId: 12d4c26c3edc0fb330898d775b3910f5) + native: #04 pc 0010d4f4 /system/lib64/libgui.so (android::AsyncWorker::run+112) (BuildId: 67cb3d0bd5575f4f3fc95aac325f4723) + native: #05 pc 000ed3a8 /system/lib64/libgui.so (void* std::__1::__thread_proxy >, void , android::AsyncWorker*> >+72) (BuildId: 67cb3d0bd5575f4f3fc95aac325f4723) + native: #06 pc 000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + native: #07 pc 0006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c) + +Zygote loaded classes=24859 post zygote classes=5561 +Dumping registered class loaders +#0 dalvik.system.PathClassLoader: [], parent #1 +#1 java.lang.BootClassLoader: [], no parent +#2 dalvik.system.PathClassLoader: [/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes27.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes16.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes7.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes19.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes5.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes10.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes28.dex:/data/data/com.datadog.android.sample/code_cache/.overlay/base.apk/classes12.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes26.dex:/data/data/com.datadog.android.sample/code_cache/.overlay/base.apk/classes14.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes30.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes29.dex:/data/data/com.datadog.android.sample/code_cache/.overlay/base.apk/classes23.dex:/data/data/com.datadog.android.sample/code_cache/.overlay/base.apk/classes13.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes3.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes6.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes2.dex:/data/data/com.datadog.android.sample/code_cache/.overlay/base.apk/classes9.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes24.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes17.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes4.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes22.dex:/data/data/com.datadog.android.sample/code_cache/.overlay/base.apk/classes8.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes20.dex:/data/data/com.datadog.android.sample/code_cache/.overlay/base.apk/classes15.dex:/data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/base.apk!classes11.dex], parent #1 +Done dumping class loaders +Classes initialized: 0 in 0 +Intern table: 45803 strong; 1045 weak +JNI: CheckJNI is on; globals=408 (plus 147 weak) +Libraries: /data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/lib/arm64/libdatadog-native-lib.so /data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/lib/arm64/libdatadog-native-sample-lib.so /data/app/~~4Hdpw4yH6PultdkbudOeDw==/com.datadog.android.sample-ivPL6XVQGCUDQXXkvycxDw==/lib/arm64/librealmc.so libandroid.so libaudioeffect_jni.so libcompiler_rt.so libframework-connectivity-jni.so libframework-connectivity-tiramisu-jni.so libicu_jni.so libjavacore.so libjavacrypto.so libjnigraphics.so libmedia_jni.so libopenjdk.so librs_jni.so librtp_jni.so libsoundpool.so libstats_jni.so libwebviewchromium_loader.so (19) +Heap: 32% free, 8401KB/12MB; 214361 objects +Image spaces: +/system/framework/arm64/boot.art +/system/framework/arm64/boot-core-libart.art +/system/framework/arm64/boot-okhttp.art +/system/framework/arm64/boot-bouncycastle.art +/system/framework/arm64/boot-apache-xml.art +/system/framework/arm64/boot-framework.art +/system/framework/arm64/boot-framework-graphics.art +/system/framework/arm64/boot-ext.art +/system/framework/arm64/boot-telephony-common.art +/system/framework/arm64/boot-voip-common.art +/system/framework/arm64/boot-ims-common.art +/system/framework/arm64/boot-core-icu4j.art +/system/framework/arm64/boot-framework-adservices.art +Dumping cumulative Gc timings +Start Dumping Averages for 1 iterations for concurrent copying +MarkingPhase: Sum: 7.269ms Avg: 7.269ms +Process mark stacks and References: Sum: 6.725ms Avg: 6.725ms +VisitConcurrentRoots: Sum: 4.812ms Avg: 4.812ms +ScanImmuneSpaces: Sum: 1.578ms Avg: 1.578ms +ClearFromSpace: Sum: 509us Avg: 509us +CaptureThreadRootsForMarking: Sum: 496us Avg: 496us +GrayAllDirtyImmuneObjects: Sum: 352us Avg: 352us +SweepLargeObjects: Sum: 238us Avg: 238us +EnqueueFinalizerReferences: Sum: 182us Avg: 182us +InitializePhase: Sum: 181us Avg: 181us +FlipOtherThreads: Sum: 150us Avg: 150us +SweepSystemWeaks: Sum: 130us Avg: 130us +MarkZygoteLargeObjects: Sum: 107us Avg: 107us +CopyingPhase: Sum: 88us Avg: 88us +ScanCardsForSpace: Sum: 82us Avg: 82us +ProcessReferences: Sum: 74us Avg: 74us +ForwardSoftReferences: Sum: 56us Avg: 56us +RecordFree: Sum: 30us Avg: 30us +VisitNonThreadRoots: Sum: 19us Avg: 19us +MarkStackAsLive: Sum: 17us Avg: 17us +(Paused)GrayAllNewlyDirtyImmuneObjects: Sum: 14us Avg: 14us +(Paused)ClearCards: Sum: 13us Avg: 13us +SweepAllocSpace: Sum: 9us Avg: 9us +SwapBitmaps: Sum: 8us Avg: 8us +ThreadListFlip: Sum: 7us Avg: 7us +ResumeRunnableThreads: Sum: 4us Avg: 4us +EmptyRBMarkBitStack: Sum: 3us Avg: 3us +ReclaimPhase: Sum: 2us Avg: 2us +(Paused)SetFromSpace: Sum: 1us Avg: 1us +(Paused)FlipCallback: Sum: 1us Avg: 1us +UnBindBitmaps: Sum: 1us Avg: 1us +Sweep: Sum: 1us Avg: 1us +FlipThreadRoots: Sum: 0 Avg: 0 +ResumeOtherThreads: Sum: 0 Avg: 0 +Done Dumping Averages +concurrent copying paused: Sum: 55us 99% C.I. 12us-43us Avg: 27.500us Max: 43us +concurrent copying freed-bytes: Avg: 14MB Max: 14MB Min: 14MB +Freed-bytes histogram: 14080:1 +concurrent copying total time: 23.159ms mean time: 23.159ms +concurrent copying freed: 221856 objects with total size 14MB +concurrent copying throughput: 9.64591e+06/s / 613MB/s per cpu-time: 672604000/s / 641MB/s +concurrent copying tracing throughput: 180MB/s per cpu-time: 189MB/s +Average major GC reclaim bytes ratio 1.59517 over 1 GC cycles +Average major GC copied live bytes ratio 0.674099 over 6 major GCs +Cumulative bytes moved 31923792 +Cumulative objects moved 646089 +Peak regions allocated 98 (24MB) / 768 (192MB) +Total madvise time 1.311ms +Start Dumping Averages for 1 iterations for young concurrent copying +CopyingPhase: Sum: 4.110ms Avg: 4.110ms +VisitConcurrentRoots: Sum: 1.229ms Avg: 1.229ms +ScanImmuneSpaces: Sum: 905us Avg: 905us +ScanCardsForSpace: Sum: 869us Avg: 869us +Process mark stacks and References: Sum: 861us Avg: 861us +InitializePhase: Sum: 311us Avg: 311us +SweepArray: Sum: 110us Avg: 110us +ClearFromSpace: Sum: 107us Avg: 107us +FlipOtherThreads: Sum: 79us Avg: 79us +GrayAllDirtyImmuneObjects: Sum: 73us Avg: 73us +EnqueueFinalizerReferences: Sum: 71us Avg: 71us +SweepSystemWeaks: Sum: 60us Avg: 60us +ThreadListFlip: Sum: 57us Avg: 57us +ProcessReferences: Sum: 32us Avg: 32us +ResumeRunnableThreads: Sum: 15us Avg: 15us +(Paused)ClearCards: Sum: 11us Avg: 11us +ForwardSoftReferences: Sum: 9us Avg: 9us +(Paused)GrayAllNewlyDirtyImmuneObjects: Sum: 7us Avg: 7us +RecordFree: Sum: 6us Avg: 6us +EmptyRBMarkBitStack: Sum: 5us Avg: 5us +VisitNonThreadRoots: Sum: 4us Avg: 4us +SwapBitmaps: Sum: 3us Avg: 3us +ResetStack: Sum: 2us Avg: 2us +ReclaimPhase: Sum: 2us Avg: 2us +MarkZygoteLargeObjects: Sum: 2us Avg: 2us +(Paused)SetFromSpace: Sum: 1us Avg: 1us +UnBindBitmaps: Sum: 1us Avg: 1us +FlipThreadRoots: Sum: 0 Avg: 0 +ResumeOtherThreads: Sum: 0 Avg: 0 +FreeList: Sum: 0 Avg: 0 +(Paused)FlipCallback: Sum: 0 Avg: 0 +Done Dumping Averages +young concurrent copying paused: Sum: 101us 99% C.I. 7us-94us Avg: 50.500us Max: 94us +young concurrent copying freed-bytes: Avg: 5765KB Max: 5765KB Min: 5765KB +Freed-bytes histogram: 5760:1 +young concurrent copying total time: 8.942ms mean time: 8.942ms +young concurrent copying freed: 81642 objects with total size 5765KB +young concurrent copying throughput: 1.02052e+07/s / 703MB/s per cpu-time: 1475870000/s / 1407MB/s +young concurrent copying tracing throughput: 137MB/s per cpu-time: 275MB/s +Average minor GC reclaim bytes ratio 0.574598 over 1 GC cycles +Average minor GC copied live bytes ratio 0.275709 over 2 minor GCs +Cumulative bytes moved 2303440 +Cumulative objects moved 50875 +Peak regions allocated 98 (24MB) / 768 (192MB) +Total time spent in GC: 32.101ms +Mean GC size throughput: 614MB/s per cpu-time: 728MB/s +Mean GC object throughput: 9.45447e+06 objects/s +Total number of allocations 517859 +Total bytes allocated 27MB +Total bytes freed 19MB +Free memory 4127KB +Free memory until GC 4127KB +Free memory until OOME 183MB +Total memory 12MB +Max memory 192MB +Zygote space size 6252KB +Total mutator paused time: 156us +Total time waiting for GC to complete: 1.917us +Total GC count: 2 +Total GC time: 32.101ms +Total blocking GC count: 0 +Total blocking GC time: 0 +Total pre-OOME GC count: 0 +Histogram of GC count per 10000 ms: 0:1,1:1 +Histogram of blocking GC count per 10000 ms: 0:2 +Native bytes total: 40103759 registered: 1245599 +Total native bytes at last GC: 24504363 +/system/framework/oat/arm64/android.hidl.manager-V1.0-java.odex: verify +/system/framework/oat/arm64/android.hidl.base-V1.0-java.odex: verify +/system_ext/framework/oat/arm64/androidx.window.sidecar.odex: verify +/system/framework/oat/arm64/android.test.base.odex: verify +/system_ext/framework/oat/arm64/androidx.window.extensions.odex: verify +Current JIT code cache size (used / resident): 67KB / 72KB +Current JIT data cache size (used / resident): 44KB / 72KB +Zygote JIT code cache size (at point of fork): 1KB / 32KB +Zygote JIT data cache size (at point of fork): 1KB / 32KB +Current JIT mini-debug-info size: 15KB +Current JIT capacity: 256KB +Current number of JIT JNI stub entries: 5 +Current number of JIT code cache entries: 66 +Total number of JIT baseline compilations: 64 +Total number of JIT optimized compilations: 16 +Total number of JIT compilations for on stack replacement: 2 +Total number of JIT code cache collections: 2 +Memory used for stack maps: Avg: 233B Max: 4096B Min: 16B +Memory used for compiled code: Avg: 952B Max: 12KB Min: 164B +Memory used for profiling info: Avg: 170B Max: 1896B Min: 24B +Start Dumping Averages for 86 iterations for JIT timings +Compiling baseline: Sum: 21.688ms Avg: 252.186us +Compiling optimized: Sum: 5.850ms Avg: 68.023us +Code cache collection: Sum: 3.235ms Avg: 37.616us +TrimMaps: Sum: 881us Avg: 10.244us +Compiling OSR: Sum: 541us Avg: 6.290us +Done Dumping Averages +Memory used for compilation: Avg: 56KB Max: 812KB Min: 5400B +ProfileSaver total_bytes_written=0 +ProfileSaver total_number_of_writes=0 +ProfileSaver total_number_of_code_cache_queries=0 +ProfileSaver total_number_of_skipped_writes=0 +ProfileSaver total_number_of_failed_writes=0 +ProfileSaver total_ms_of_sleep=4629 +ProfileSaver total_ms_of_work=0 +ProfileSaver total_number_of_hot_spikes=1 +ProfileSaver total_number_of_wake_ups=2 + +*** ART internal metrics *** + Metadata: + timestamp_since_start_ms: 30667 + Metrics: + ClassLoadingTotalTime: count = 77422 + ClassVerificationTotalTime: count = 302725 + ClassVerificationCount: count = 2680 + WorldStopTimeDuringGCAvg: count = 78 + YoungGcCount: count = 1 + FullGcCount: count = 1 + TotalBytesAllocated: count = 26795048 + TotalGcCollectionTime: count = 32 + YoungGcThroughputAvg: count = 418 + FullGcThroughputAvg: count = 501 + YoungGcTracingThroughputAvg: count = 120 + FullGcTracingThroughputAvg: count = 178 + JitMethodCompileTotalTime: count = 33920 + JitMethodCompileCount: count = 82 + YoungGcCollectionTime: range = 0...60000, buckets: 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + FullGcCollectionTime: range = 0...60000, buckets: 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + YoungGcThroughput: range = 0...10000, buckets: 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + FullGcThroughput: range = 0...10000, buckets: 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + YoungGcTracingThroughput: range = 0...10000, buckets: 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + FullGcTracingThroughput: range = 0...10000, buckets: 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + GcWorldStopTime: count = 156 + GcWorldStopCount: count = 2 + YoungGcScannedBytes: count = 1154156 + YoungGcFreedBytes: count = 4392056 + YoungGcDuration: count = 9 + FullGcScannedBytes: count = 4362496 + FullGcFreedBytes: count = 12626408 + FullGcDuration: count = 23 + GcWorldStopTimeDelta: count = 156 + GcWorldStopCountDelta: count = 2 + YoungGcScannedBytesDelta: count = 1154156 + YoungGcFreedBytesDelta: count = 4392056 + YoungGcDurationDelta: count = 9 + FullGcScannedBytesDelta: count = 4362496 + FullGcFreedBytesDelta: count = 12626408 + FullGcDurationDelta: count = 23 + JitMethodCompileTotalTimeDelta: count = 33920 + JitMethodCompileCountDelta: count = 82 + ClassVerificationTotalTimeDelta: count = 302725 + ClassVerificationCountDelta: count = 2680 + ClassLoadingTotalTimeDelta: count = 76928 + TotalBytesAllocatedDelta: count = 26795048 + TotalGcCollectionTimeDelta: count = 32 + YoungGcCountDelta: count = 1 + FullGcCountDelta: count = 1 +*** Done dumping ART internal metrics *** + +----- end 10348 ----- + +----- Waiting Channels: pid 10348 at 2024-02-13 11:43:44.465482634+0100 ----- +Cmd line: com.datadog.android.sample + +sysTid=10348 0 +sysTid=10354 do_sigtimedwait +sysTid=10355 pipe_read +sysTid=10356 do_sys_poll +sysTid=10357 futex_wait +sysTid=10358 futex_wait +sysTid=10359 futex_wait +sysTid=10360 futex_wait +sysTid=10361 futex_wait +sysTid=10362 binder_ioctl_write_read +sysTid=10363 binder_ioctl_write_read +sysTid=10365 binder_ioctl_write_read +sysTid=10368 futex_wait +sysTid=10369 futex_wait +sysTid=10370 __skb_wait_for_more_packets +sysTid=10372 futex_wait +sysTid=10374 futex_wait +sysTid=10378 futex_wait +sysTid=10379 do_epoll_wait +sysTid=10380 futex_wait +sysTid=10381 futex_wait +sysTid=10382 futex_wait +sysTid=10383 futex_wait +sysTid=10384 do_epoll_wait +sysTid=10385 futex_wait +sysTid=10386 do_epoll_wait +sysTid=10387 do_epoll_wait +sysTid=10388 futex_wait +sysTid=10389 futex_wait +sysTid=10390 futex_wait +sysTid=10391 futex_wait +sysTid=10392 0 +sysTid=10395 futex_wait +sysTid=10396 do_epoll_wait +sysTid=10397 do_epoll_wait +sysTid=10399 futex_wait +sysTid=10400 futex_wait +sysTid=10401 binder_ioctl_write_read +sysTid=10402 futex_wait +sysTid=10404 futex_wait +sysTid=10405 futex_wait +sysTid=10426 futex_wait +sysTid=10430 wait_woken +sysTid=10431 futex_wait +sysTid=10432 futex_wait +sysTid=10433 futex_wait +sysTid=10443 futex_wait + +----- end 10348 ----- + + diff --git a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaskInputTabWireframeMapper.kt b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaskInputTabWireframeMapper.kt index f2afbc9510..6d51419674 100644 --- a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaskInputTabWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaskInputTabWireframeMapper.kt @@ -10,12 +10,34 @@ import android.widget.TextView import com.datadog.android.sessionreplay.internal.recorder.mapper.MaskInputTextViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver -internal class MaskInputTabWireframeMapper( - viewUtils: ViewUtils = ViewUtils, - uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator, - textViewMapper: WireframeMapper = - MaskInputTextViewMapper() -) : TabWireframeMapper(viewUtils, uniqueIdentifierGenerator, textViewMapper) +internal class MaskInputTabWireframeMapper internal constructor( + viewIdentifierResolver: ViewIdentifierResolver, + viewBoundsResolver: ViewBoundsResolver, + textViewMapper: WireframeMapper +) : TabWireframeMapper( + viewIdentifierResolver, + viewBoundsResolver, + textViewMapper +) { + + constructor( + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper + ) : this( + viewIdentifierResolver, + viewBoundsResolver, + MaskInputTextViewMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + ) +} diff --git a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaskSliderWireframeMapper.kt b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaskSliderWireframeMapper.kt index 792742cd62..39c9f50be2 100644 --- a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaskSliderWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaskSliderWireframeMapper.kt @@ -7,16 +7,15 @@ package com.datadog.android.sessionreplay.material import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal open class MaskSliderWireframeMapper( - viewUtils: ViewUtils = ViewUtils, - stringUtils: StringUtils = StringUtils, - uniqueIdentifierGenerator: UniqueIdentifierGenerator = - UniqueIdentifierGenerator -) : SliderWireframeMapper(viewUtils, stringUtils, uniqueIdentifierGenerator) { + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver +) : SliderWireframeMapper(viewIdentifierResolver, colorStringFormatter, viewBoundsResolver) { override fun resolveViewAsWireframesList( nonActiveTrackWireframe: MobileSegment.Wireframe.ShapeWireframe, diff --git a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaskTabWireframeMapper.kt b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaskTabWireframeMapper.kt index 30fded2dbc..7f6993fedf 100644 --- a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaskTabWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaskTabWireframeMapper.kt @@ -10,12 +10,34 @@ import android.widget.TextView import com.datadog.android.sessionreplay.internal.recorder.mapper.MaskTextViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver -internal class MaskTabWireframeMapper( - viewUtils: ViewUtils = ViewUtils, - uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator, - textViewMapper: WireframeMapper = - MaskTextViewMapper() -) : TabWireframeMapper(viewUtils, uniqueIdentifierGenerator, textViewMapper) +internal class MaskTabWireframeMapper internal constructor( + viewIdentifierResolver: ViewIdentifierResolver, + viewBoundsResolver: ViewBoundsResolver, + textViewMapper: WireframeMapper +) : TabWireframeMapper( + viewIdentifierResolver, + viewBoundsResolver, + textViewMapper +) { + + constructor( + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper + ) : this( + viewIdentifierResolver, + viewBoundsResolver, + MaskTextViewMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + ) +} diff --git a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupport.kt b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupport.kt index 9913f6636c..e0cf1929dd 100644 --- a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupport.kt +++ b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupport.kt @@ -10,7 +10,16 @@ import android.view.View import com.datadog.android.sessionreplay.ExtensionSupport import com.datadog.android.sessionreplay.SessionReplayPrivacy import com.datadog.android.sessionreplay.internal.recorder.OptionSelectorDetector +import com.datadog.android.sessionreplay.internal.recorder.mapper.MaskTextViewMapper +import com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter +import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver +import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import com.google.android.material.slider.Slider import com.google.android.material.tabs.TabLayout @@ -19,13 +28,49 @@ import com.google.android.material.tabs.TabLayout * configuration. */ class MaterialExtensionSupport : ExtensionSupport { + + private val viewIdentifierResolver: ViewIdentifierResolver = DefaultViewIdentifierResolver + private val colorStringFormatter: ColorStringFormatter = DefaultColorStringFormatter + private val viewBoundsResolver: ViewBoundsResolver = DefaultViewBoundsResolver + private val drawableToColorMapper: DrawableToColorMapper = DrawableToColorMapper.getDefault() + @Suppress("UNCHECKED_CAST") override fun getCustomViewMappers(): Map, WireframeMapper>> { - val maskUserInputSliderMapper = MaskSliderWireframeMapper() as WireframeMapper - val maskSliderMapper = MaskSliderWireframeMapper() as WireframeMapper - val allowSliderMapper = SliderWireframeMapper() as WireframeMapper - val maskTabWireframeMapper = MaskTabWireframeMapper() as WireframeMapper - val allowTabWireframeMapper = TabWireframeMapper() as WireframeMapper + val maskUserInputSliderMapper = MaskSliderWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver + ) as WireframeMapper + val maskSliderMapper = MaskSliderWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver + ) as WireframeMapper + val allowSliderMapper = SliderWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver + ) as WireframeMapper + + val maskTabWireframeMapper = + MaskTabWireframeMapper( + viewIdentifierResolver, + viewBoundsResolver, + MaskTextViewMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + ) as WireframeMapper + + val allowTabWireframeMapper = + TabWireframeMapper( + viewIdentifierResolver, + viewBoundsResolver, + TextViewMapper(viewIdentifierResolver, colorStringFormatter, viewBoundsResolver, drawableToColorMapper) + ) as WireframeMapper + return mapOf( SessionReplayPrivacy.ALLOW to mapOf( Slider::class.java to allowSliderMapper, diff --git a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/SliderWireframeMapper.kt b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/SliderWireframeMapper.kt index 0d917368aa..eb66ce1ca9 100644 --- a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/SliderWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/SliderWireframeMapper.kt @@ -7,39 +7,37 @@ package com.datadog.android.sessionreplay.material import android.content.res.ColorStateList -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper import com.datadog.android.sessionreplay.material.internal.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import com.google.android.material.slider.Slider internal open class SliderWireframeMapper( - private val viewUtils: ViewUtils = ViewUtils, - private val stringUtils: StringUtils = StringUtils, - private val uniqueIdentifierGenerator: UniqueIdentifierGenerator = - UniqueIdentifierGenerator -) : - WireframeMapper { + private val viewIdentifierResolver: ViewIdentifierResolver, + private val colorStringFormatter: ColorStringFormatter, + private val viewBoundsResolver: ViewBoundsResolver +) : WireframeMapper { @Suppress("LongMethod") override fun map(view: Slider, mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback): List { - val activeTrackId = uniqueIdentifierGenerator + val activeTrackId = viewIdentifierResolver .resolveChildUniqueIdentifier(view, TRACK_ACTIVE_KEY_NAME) - val nonActiveTrackId = uniqueIdentifierGenerator + val nonActiveTrackId = viewIdentifierResolver .resolveChildUniqueIdentifier(view, TRACK_NON_ACTIVE_KEY_NAME) - val thumbId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier(view, THUMB_KEY_NAME) + val thumbId = viewIdentifierResolver.resolveChildUniqueIdentifier(view, THUMB_KEY_NAME) if (activeTrackId == null || thumbId == null || nonActiveTrackId == null) { return emptyList() } val screenDensity = mappingContext.systemInformation.screenDensity - val viewGlobalBounds = viewUtils.resolveViewGlobalBounds(view, screenDensity) + val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds(view, screenDensity) val normalizedSliderValue = view.normalizedValue() val viewAlpha = view.alpha @@ -52,15 +50,15 @@ internal open class SliderWireframeMapper( val trackActiveColor = view.trackActiveTintList.getColor(drawableState) val trackNonActiveColor = view.trackInactiveTintList.getColor(drawableState) val thumbColor = view.thumbTintList.getColor(drawableState) - val trackActiveColorAsHexa = stringUtils.formatColorAndAlphaAsHexa( + val trackActiveColorAsHexa = colorStringFormatter.formatColorAndAlphaAsHexString( trackActiveColor, OPAQUE_ALPHA_VALUE ) - val trackNonActiveColorAsHexa = stringUtils.formatColorAndAlphaAsHexa( + val trackNonActiveColorAsHexa = colorStringFormatter.formatColorAndAlphaAsHexString( trackNonActiveColor, PARTIALLY_OPAQUE_ALPHA_VALUE ) - val thumbColorAsHexa = stringUtils.formatColorAndAlphaAsHexa(thumbColor, OPAQUE_ALPHA_VALUE) + val thumbColorAsHexa = colorStringFormatter.formatColorAndAlphaAsHexString(thumbColor, OPAQUE_ALPHA_VALUE) // track dimensions val trackWidth = view.trackWidth.toLong().densityNormalized(screenDensity) diff --git a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/TabWireframeMapper.kt b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/TabWireframeMapper.kt index ffe9e8e133..e5960f7e13 100644 --- a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/TabWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/TabWireframeMapper.kt @@ -7,25 +7,42 @@ package com.datadog.android.sessionreplay.material import android.widget.TextView -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.SystemInformation import com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper import com.datadog.android.sessionreplay.material.internal.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout.TabView -internal open class TabWireframeMapper( - private val viewUtils: ViewUtils = ViewUtils, - private val uniqueIdentifierGenerator: UniqueIdentifierGenerator = - UniqueIdentifierGenerator, - internal val textViewMapper: WireframeMapper = TextViewMapper() +internal open class TabWireframeMapper internal constructor( + private val viewIdentifierResolver: ViewIdentifierResolver, + private val viewBoundsResolver: ViewBoundsResolver, + internal val textViewMapper: WireframeMapper ) : WireframeMapper { + constructor( + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper + ) : this( + viewIdentifierResolver, + viewBoundsResolver, + TextViewMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + ) + override fun map( view: TabView, mappingContext: MappingContext, @@ -54,12 +71,12 @@ internal open class TabWireframeMapper( systemInformation: SystemInformation, wireframe: MobileSegment.Wireframe? ): MobileSegment.Wireframe? { - val selectorId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + val selectorId = viewIdentifierResolver.resolveChildUniqueIdentifier( view, SELECTED_TAB_INDICATOR_KEY_NAME ) ?: return null val screenDensity = systemInformation.screenDensity - val viewBounds = viewUtils.resolveViewGlobalBounds(view, screenDensity) + val viewBounds = viewBoundsResolver.resolveViewGlobalBounds(view, screenDensity) val selectionIndicatorHeight = SELECTED_TAB_INDICATOR_HEIGHT_IN_PX .densityNormalized(screenDensity) val paddingStart = view.paddingStart.toLong().densityNormalized(screenDensity) diff --git a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/BaseSliderWireframeMapperTest.kt b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/BaseSliderWireframeMapperTest.kt index 941e969a52..dba4d7cca6 100644 --- a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/BaseSliderWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/BaseSliderWireframeMapperTest.kt @@ -7,12 +7,13 @@ package com.datadog.android.sessionreplay.material import android.content.res.ColorStateList -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.material.internal.densityNormalized -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import com.google.android.material.slider.Slider import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.FloatForgery @@ -137,13 +138,16 @@ internal abstract class BaseSliderWireframeMapperTest { lateinit var mockTrackNotActiveTintColors: ColorStateList @Mock - lateinit var mockViewUtils: ViewUtils + lateinit var mockViewIdentifierResolver: ViewIdentifierResolver @Mock - lateinit var mockStringUtils: StringUtils + lateinit var mockColorStringFormatter: ColorStringFormatter @Mock - lateinit var mockUniqueIdentifierGenerator: UniqueIdentifierGenerator + lateinit var mockViewBoundsResolver: ViewBoundsResolver + + @Mock + lateinit var mockAsyncJobStatusCallback: AsyncJobStatusCallback lateinit var testedSliderWireframeMapper: SliderWireframeMapper @@ -192,7 +196,7 @@ internal abstract class BaseSliderWireframeMapperTest { fakeExpectedThumbYPos = normalizedSliderYPos + normalizedSliderTopPadding + (normalizedSliderHeight - fakeExpectedThumbHeight) / 2 whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( fakeThumbColor, SliderWireframeMapper.OPAQUE_ALPHA_VALUE ) @@ -200,14 +204,14 @@ internal abstract class BaseSliderWireframeMapperTest { .thenReturn(fakeExpectedThumbHtmlColor) whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( fakeTrackActiveColor, SliderWireframeMapper.OPAQUE_ALPHA_VALUE ) ) .thenReturn(fakeExpectedTrackActiveHtmlColor) whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( fakeTrackNotActiveColor, SliderWireframeMapper.PARTIALLY_OPAQUE_ALPHA_VALUE ) @@ -216,26 +220,26 @@ internal abstract class BaseSliderWireframeMapperTest { mockSlider = generateMockedSlider(forge) whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockSlider, fakeMappingContext.systemInformation.screenDensity ) ) .thenReturn(fakeViewGlobalBounds) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSlider, SliderWireframeMapper.TRACK_ACTIVE_KEY_NAME ) ).thenReturn(fakeActiveTrackId) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSlider, SliderWireframeMapper.TRACK_NON_ACTIVE_KEY_NAME ) ).thenReturn(fakeInactiveTrackId) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSlider, SliderWireframeMapper.THUMB_KEY_NAME ) @@ -249,42 +253,60 @@ internal abstract class BaseSliderWireframeMapperTest { fun `M return empty list W map { could not generate thumb id`() { // Given whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSlider, SliderWireframeMapper.THUMB_KEY_NAME ) ).thenReturn(null) // Then - assertThat(testedSliderWireframeMapper.map(mockSlider, fakeMappingContext)).isEmpty() + assertThat( + testedSliderWireframeMapper.map( + mockSlider, + fakeMappingContext, + mockAsyncJobStatusCallback + ) + ).isEmpty() } @Test fun `M return empty list W map { could not generate active track id`() { // Given whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSlider, SliderWireframeMapper.TRACK_ACTIVE_KEY_NAME ) ).thenReturn(null) // Then - assertThat(testedSliderWireframeMapper.map(mockSlider, fakeMappingContext)).isEmpty() + assertThat( + testedSliderWireframeMapper.map( + mockSlider, + fakeMappingContext, + mockAsyncJobStatusCallback + ) + ).isEmpty() } @Test fun `M return empty list W map { could not generate inactive track id`() { // Given whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSlider, SliderWireframeMapper.TRACK_NON_ACTIVE_KEY_NAME ) ).thenReturn(null) // Then - assertThat(testedSliderWireframeMapper.map(mockSlider, fakeMappingContext)).isEmpty() + assertThat( + testedSliderWireframeMapper.map( + mockSlider, + fakeMappingContext, + mockAsyncJobStatusCallback + ) + ).isEmpty() } private fun generateMockedSlider(forge: Forge): Slider { diff --git a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/BaseTabWireframeMapperTest.kt b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/BaseTabWireframeMapperTest.kt index 0a490ac1e4..76483330a7 100644 --- a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/BaseTabWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/BaseTabWireframeMapperTest.kt @@ -7,13 +7,16 @@ package com.datadog.android.sessionreplay.material import android.widget.TextView -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper import com.datadog.android.sessionreplay.material.internal.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import com.google.android.material.tabs.TabLayout.TabView import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery @@ -33,15 +36,9 @@ internal abstract class BaseTabWireframeMapperTest { @Forgery lateinit var fakeMappingContext: MappingContext - @Mock - lateinit var mockUniqueIdentifierGenerator: UniqueIdentifierGenerator - @Forgery lateinit var fakeGlobalBounds: GlobalBounds - @Mock - lateinit var mockViewUtils: ViewUtils - @Mock lateinit var mockTextWireframeMapper: TextViewMapper @@ -63,18 +60,33 @@ internal abstract class BaseTabWireframeMapperTest { @IntForgery(min = 0, max = 10) var fakePaddingEnd: Int = 0 + @Mock + lateinit var mockViewIdentifierResolver: ViewIdentifierResolver + + @Mock + lateinit var mockColorStringFormatter: ColorStringFormatter + + @Mock + lateinit var mockViewBoundsResolver: ViewBoundsResolver + + @Mock + lateinit var mockDrawableToColorMapper: DrawableToColorMapper + + @Mock + lateinit var mockAsyncJobStatusCallback: AsyncJobStatusCallback + @BeforeEach fun `set up`(forge: Forge) { fakeTextWireframes = forge.aList(size = 1) { getForgery() } mockTabView = forge.mockTabView() whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockTabView, fakeMappingContext.systemInformation.screenDensity ) ).thenReturn(fakeGlobalBounds) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockTabView, TabWireframeMapper.SELECTED_TAB_INDICATOR_KEY_NAME ) @@ -116,7 +128,7 @@ internal abstract class BaseTabWireframeMapperTest { val expectedMappedWireframes = fakeTextWireframes + expectedTabIndicatorWireframe // When - val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext) + val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(mappedWireframes).isEqualTo(expectedMappedWireframes) @@ -129,7 +141,7 @@ internal abstract class BaseTabWireframeMapperTest { val expectedMappedWireframes = fakeTextWireframes // When - val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext) + val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(mappedWireframes).isEqualTo(expectedMappedWireframes) @@ -167,7 +179,7 @@ internal abstract class BaseTabWireframeMapperTest { val expectedMappedWireframes = listOf(expectedTabIndicatorWireframe) // When - val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext) + val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(mappedWireframes).isEqualTo(expectedMappedWireframes) @@ -182,7 +194,7 @@ internal abstract class BaseTabWireframeMapperTest { whenever(mockTabView.isSelected).thenReturn(false) // When - val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext) + val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(mappedWireframes).isEmpty() @@ -218,7 +230,7 @@ internal abstract class BaseTabWireframeMapperTest { val expectedMappedWireframes = listOf(expectedTabIndicatorWireframe) // When - val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext) + val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(mappedWireframes).isEqualTo(expectedMappedWireframes) @@ -231,7 +243,7 @@ internal abstract class BaseTabWireframeMapperTest { whenever(mockTabView.isSelected).thenReturn(false) // When - val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext) + val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(mappedWireframes).isEmpty() @@ -240,13 +252,13 @@ internal abstract class BaseTabWireframeMapperTest { @Test fun `M map the Tab to a list of wireframes W map() { tab not selected, id generate failed }`() { // Given - whenever(mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier(any(), any())) + whenever(mockViewIdentifierResolver.resolveChildUniqueIdentifier(any(), any())) .thenReturn(null) whenever(mockTabView.isSelected).thenReturn(false) val expectedMappedWireframes = fakeTextWireframes // When - val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext) + val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(mappedWireframes).isEqualTo(expectedMappedWireframes) @@ -255,13 +267,13 @@ internal abstract class BaseTabWireframeMapperTest { @Test fun `M map the Tab to a list of wireframes W map() { tab selected, id generate failed }`() { // Given - whenever(mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier(any(), any())) + whenever(mockViewIdentifierResolver.resolveChildUniqueIdentifier(any(), any())) .thenReturn(null) whenever(mockTabView.isSelected).thenReturn(true) val expectedMappedWireframes = fakeTextWireframes // When - val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext) + val mappedWireframes = testedTabWireframeMapper.map(mockTabView, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(mappedWireframes).isEqualTo(expectedMappedWireframes) diff --git a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/MaskSliderWireframeMapperTest.kt b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/MaskSliderWireframeMapperTest.kt index 342058d42b..e338a775a6 100644 --- a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/MaskSliderWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/MaskSliderWireframeMapperTest.kt @@ -28,9 +28,9 @@ internal class MaskSliderWireframeMapperTest : BaseSliderWireframeMapperTest() { override fun provideTestInstance(): SliderWireframeMapper { return MaskSliderWireframeMapper( - viewUtils = mockViewUtils, - stringUtils = mockStringUtils, - uniqueIdentifierGenerator = mockUniqueIdentifierGenerator + viewBoundsResolver = mockViewBoundsResolver, + colorStringFormatter = mockColorStringFormatter, + viewIdentifierResolver = mockViewIdentifierResolver ) } @@ -50,7 +50,8 @@ internal class MaskSliderWireframeMapperTest : BaseSliderWireframeMapperTest() { ) // When - val mappedWireframes = testedSliderWireframeMapper.map(mockSlider, fakeMappingContext) + val mappedWireframes = + testedSliderWireframeMapper.map(mockSlider, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(mappedWireframes).isEqualTo(listOf(expectedInactiveTrackWireframe)) diff --git a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/MaskTabWireframeMapperTest.kt b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/MaskTabWireframeMapperTest.kt index 56dcf71450..497405b12f 100644 --- a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/MaskTabWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/MaskTabWireframeMapperTest.kt @@ -28,8 +28,8 @@ internal class MaskTabWireframeMapperTest : BaseTabWireframeMapperTest() { override fun provideTestInstance(): TabWireframeMapper { return MaskTabWireframeMapper( - viewUtils = mockViewUtils, - uniqueIdentifierGenerator = mockUniqueIdentifierGenerator, + viewIdentifierResolver = mockViewIdentifierResolver, + viewBoundsResolver = mockViewBoundsResolver, textViewMapper = mockTextWireframeMapper ) } @@ -37,7 +37,12 @@ internal class MaskTabWireframeMapperTest : BaseTabWireframeMapperTest() { @Test fun `M use a MaskTextViewMapper when initialized`() { // Given - val maskTabWireframeMapper = MaskTabWireframeMapper() + val maskTabWireframeMapper = MaskTabWireframeMapper( + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper + ) // Then assertThat(maskTabWireframeMapper.textViewMapper) diff --git a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/SliderWireframeMapperTest.kt b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/SliderWireframeMapperTest.kt index 8838aee9b1..6da2216971 100644 --- a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/SliderWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/SliderWireframeMapperTest.kt @@ -27,11 +27,7 @@ import org.mockito.quality.Strictness internal class SliderWireframeMapperTest : BaseSliderWireframeMapperTest() { override fun provideTestInstance(): SliderWireframeMapper { - return SliderWireframeMapper( - viewUtils = mockViewUtils, - stringUtils = mockStringUtils, - uniqueIdentifierGenerator = mockUniqueIdentifierGenerator - ) + return SliderWireframeMapper(mockViewIdentifierResolver, mockColorStringFormatter, mockViewBoundsResolver) } @Test @@ -73,7 +69,8 @@ internal class SliderWireframeMapperTest : BaseSliderWireframeMapperTest() { ) // When - val mappedWireframes = testedSliderWireframeMapper.map(mockSlider, fakeMappingContext) + val mappedWireframes = + testedSliderWireframeMapper.map(mockSlider, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(mappedWireframes).isEqualTo( diff --git a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/TabWireframeMapperTest.kt b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/TabWireframeMapperTest.kt index 002636089f..f61353f6e3 100644 --- a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/TabWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/TabWireframeMapperTest.kt @@ -28,8 +28,8 @@ internal class TabWireframeMapperTest : BaseTabWireframeMapperTest() { override fun provideTestInstance(): TabWireframeMapper { return TabWireframeMapper( - viewUtils = mockViewUtils, - uniqueIdentifierGenerator = mockUniqueIdentifierGenerator, + viewIdentifierResolver = mockViewIdentifierResolver, + viewBoundsResolver = mockViewBoundsResolver, textViewMapper = mockTextWireframeMapper ) } @@ -37,7 +37,12 @@ internal class TabWireframeMapperTest : BaseTabWireframeMapperTest() { @Test fun `M use a TextViewMapper when initialized`() { // Given - val tabWireframeMapper = TabWireframeMapper() + val tabWireframeMapper = TabWireframeMapper( + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper + ) // Then assertThat(tabWireframeMapper.textViewMapper) diff --git a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/GlobalBoundsForgeryFactory.kt b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/GlobalBoundsForgeryFactory.kt index dbe5f27d26..a5c4332889 100644 --- a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/GlobalBoundsForgeryFactory.kt +++ b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/GlobalBoundsForgeryFactory.kt @@ -6,7 +6,7 @@ package com.datadog.android.sessionreplay.material.forge -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds +import com.datadog.android.sessionreplay.utils.GlobalBounds import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/MappingContextForgeryFactory.kt b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/MappingContextForgeryFactory.kt index 1219916d3e..1fc9ff7544 100644 --- a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/MappingContextForgeryFactory.kt +++ b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/MappingContextForgeryFactory.kt @@ -9,11 +9,13 @@ package com.datadog.android.sessionreplay.material.forge import com.datadog.android.sessionreplay.internal.recorder.MappingContext import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory +import org.mockito.kotlin.mock internal class MappingContextForgeryFactory : ForgeryFactory { override fun getForgery(forge: Forge): MappingContext { return MappingContext( systemInformation = forge.getForgery(), + imageWireframeHelper = mock(), hasOptionSelectorParent = forge.aBool() ) } diff --git a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/SystemInformationForgeryFactory.kt b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/SystemInformationForgeryFactory.kt index 3cd072c84e..25f3fe02e3 100644 --- a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/SystemInformationForgeryFactory.kt +++ b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/SystemInformationForgeryFactory.kt @@ -7,8 +7,8 @@ package com.datadog.android.sessionreplay.material.forge import android.content.res.Configuration -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.SystemInformation +import com.datadog.android.sessionreplay.utils.GlobalBounds import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/features/dd-sdk-android-session-replay/api/apiSurface b/features/dd-sdk-android-session-replay/api/apiSurface index 49bbf47575..2cc233e6cb 100644 --- a/features/dd-sdk-android-session-replay/api/apiSurface +++ b/features/dd-sdk-android-session-replay/api/apiSurface @@ -14,44 +14,76 @@ enum com.datadog.android.sessionreplay.SessionReplayPrivacy - ALLOW - MASK - MASK_USER_INPUT -interface com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback - fun jobStarted() - fun jobFinished() -data class com.datadog.android.sessionreplay.internal.recorder.GlobalBounds - constructor(Long, Long, Long, Long) data class com.datadog.android.sessionreplay.internal.recorder.MappingContext - constructor(SystemInformation, Boolean = false) + constructor(SystemInformation, com.datadog.android.sessionreplay.utils.ImageWireframeHelper, Boolean = false) interface com.datadog.android.sessionreplay.internal.recorder.OptionSelectorDetector fun isOptionSelector(android.view.ViewGroup): Boolean data class com.datadog.android.sessionreplay.internal.recorder.SystemInformation - constructor(GlobalBounds, Int = Configuration.ORIENTATION_UNDEFINED, Float, String? = null) + constructor(com.datadog.android.sessionreplay.utils.GlobalBounds, Int = Configuration.ORIENTATION_UNDEFINED, Float, String? = null) abstract class com.datadog.android.sessionreplay.internal.recorder.mapper.BaseAsyncBackgroundWireframeMapper : BaseWireframeMapper - constructor(com.datadog.android.sessionreplay.utils.StringUtils = StringUtils, com.datadog.android.sessionreplay.utils.ViewUtils = ViewUtils) - override fun map(T, com.datadog.android.sessionreplay.internal.recorder.MappingContext, com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback): List + override fun map(T, com.datadog.android.sessionreplay.internal.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback): List companion object abstract class com.datadog.android.sessionreplay.internal.recorder.mapper.BaseWireframeMapper : WireframeMapper - constructor(com.datadog.android.sessionreplay.utils.StringUtils = StringUtils, com.datadog.android.sessionreplay.utils.ViewUtils = ViewUtils) + constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper) protected fun resolveViewId(android.view.View): Long - protected fun colorAndAlphaAsStringHexa(Int, Int): String - protected fun resolveViewGlobalBounds(android.view.View, Float): com.datadog.android.sessionreplay.internal.recorder.GlobalBounds - protected fun android.graphics.drawable.Drawable.resolveShapeStyleAndBorder(Float): Pair? + protected fun resolveShapeStyle(android.graphics.drawable.Drawable, Float): com.datadog.android.sessionreplay.model.MobileSegment.ShapeStyle? companion object open class com.datadog.android.sessionreplay.internal.recorder.mapper.MaskInputTextViewMapper : TextViewMapper - constructor() + constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper) open class com.datadog.android.sessionreplay.internal.recorder.mapper.MaskTextViewMapper : TextViewMapper - constructor() + constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper) open class com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper : BaseAsyncBackgroundWireframeMapper - constructor() - override fun map(android.widget.TextView, com.datadog.android.sessionreplay.internal.recorder.MappingContext, com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback): List + constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper) + override fun map(android.widget.TextView, com.datadog.android.sessionreplay.internal.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback): List interface com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChildrenMapper : WireframeMapper interface com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper - fun map(T, com.datadog.android.sessionreplay.internal.recorder.MappingContext, com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback = NoOpAsyncJobStatusCallback()): List -object com.datadog.android.sessionreplay.utils.StringUtils - fun formatColorAndAlphaAsHexa(Int, Int): String -object com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator + fun map(T, com.datadog.android.sessionreplay.internal.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback): List +class com.datadog.android.sessionreplay.internal.recorder.obfuscator.AndroidNStringObfuscator : StringObfuscator + override fun obfuscate(String): String +open class com.datadog.android.sessionreplay.utils.AndroidMDrawableToColorMapper : LegacyDrawableToColorMapper + override fun resolveRippleDrawable(android.graphics.drawable.RippleDrawable): Int? + override fun resolveInsetDrawable(android.graphics.drawable.InsetDrawable): Int? +open class com.datadog.android.sessionreplay.utils.AndroidQDrawableToColorMapper : AndroidMDrawableToColorMapper + override fun resolveGradientDrawable(android.graphics.drawable.GradientDrawable): Int? + companion object +interface com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback + fun jobStarted() + fun jobFinished() +interface com.datadog.android.sessionreplay.utils.ColorStringFormatter + fun formatColorAsHexString(Int): String + fun formatColorAndAlphaAsHexString(Int, Int): String +object com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter : ColorStringFormatter + override fun formatColorAsHexString(Int): String + override fun formatColorAndAlphaAsHexString(Int, Int): String +object com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver : ViewBoundsResolver + override fun resolveViewGlobalBounds(android.view.View, Float): GlobalBounds +object com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver : ViewIdentifierResolver + override fun resolveViewId(android.view.View): Long + override fun resolveChildUniqueIdentifier(android.view.View, String): Long? +interface com.datadog.android.sessionreplay.utils.DrawableToColorMapper + fun mapDrawableToColor(android.graphics.drawable.Drawable): Int? + companion object + fun getDefault(): DrawableToColorMapper +data class com.datadog.android.sessionreplay.utils.GlobalBounds + constructor(Long, Long, Long, Long) +interface com.datadog.android.sessionreplay.utils.ImageWireframeHelper + fun createImageWireframe(android.view.View, Int, Long, Long, Int, Int, Boolean, android.graphics.drawable.Drawable, AsyncJobStatusCallback, com.datadog.android.sessionreplay.model.MobileSegment.WireframeClip? = null, com.datadog.android.sessionreplay.model.MobileSegment.ShapeStyle? = null, com.datadog.android.sessionreplay.model.MobileSegment.ShapeBorder? = null, String? = DRAWABLE_CHILD_NAME): com.datadog.android.sessionreplay.model.MobileSegment.Wireframe? + fun createCompoundDrawableWireframes(android.widget.TextView, com.datadog.android.sessionreplay.internal.recorder.MappingContext, Int, AsyncJobStatusCallback): MutableList + companion object +open class com.datadog.android.sessionreplay.utils.LegacyDrawableToColorMapper : DrawableToColorMapper + override fun mapDrawableToColor(android.graphics.drawable.Drawable): Int? + protected open fun resolveColorDrawable(android.graphics.drawable.ColorDrawable): Int? + protected open fun resolveRippleDrawable(android.graphics.drawable.RippleDrawable): Int? + protected open fun resolveLayerDrawable(android.graphics.drawable.LayerDrawable, (Int, android.graphics.drawable.Drawable) -> Boolean = { _, _ -> true }): Int? + protected open fun resolveGradientDrawable(android.graphics.drawable.GradientDrawable): Int? + protected open fun resolveInsetDrawable(android.graphics.drawable.InsetDrawable): Int? + protected fun mergeColorAndAlpha(Int, Int): Int + companion object +interface com.datadog.android.sessionreplay.utils.ViewBoundsResolver + fun resolveViewGlobalBounds(android.view.View, Float): GlobalBounds +interface com.datadog.android.sessionreplay.utils.ViewIdentifierResolver + fun resolveViewId(android.view.View): Long fun resolveChildUniqueIdentifier(android.view.View, String): Long? -object com.datadog.android.sessionreplay.utils.ViewUtils - fun resolveViewGlobalBounds(android.view.View, Float): com.datadog.android.sessionreplay.internal.recorder.GlobalBounds data class com.datadog.android.sessionreplay.model.MobileSegment constructor(Application, Session, View, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long? = null, kotlin.Boolean? = null, Source, kotlin.collections.List) fun toJson(): com.google.gson.JsonElement diff --git a/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api b/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api index 45a2720b51..7c37c7bd66 100644 --- a/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api +++ b/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api @@ -34,37 +34,17 @@ public final class com/datadog/android/sessionreplay/SessionReplayPrivacy : java public static fun values ()[Lcom/datadog/android/sessionreplay/SessionReplayPrivacy; } -public abstract interface class com/datadog/android/sessionreplay/internal/AsyncJobStatusCallback { - public abstract fun jobFinished ()V - public abstract fun jobStarted ()V -} - -public final class com/datadog/android/sessionreplay/internal/recorder/GlobalBounds { - public fun (JJJJ)V - public final fun component1 ()J - public final fun component2 ()J - public final fun component3 ()J - public final fun component4 ()J - public final fun copy (JJJJ)Lcom/datadog/android/sessionreplay/internal/recorder/GlobalBounds; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/internal/recorder/GlobalBounds;JJJJILjava/lang/Object;)Lcom/datadog/android/sessionreplay/internal/recorder/GlobalBounds; - public fun equals (Ljava/lang/Object;)Z - public final fun getHeight ()J - public final fun getWidth ()J - public final fun getX ()J - public final fun getY ()J - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - public final class com/datadog/android/sessionreplay/internal/recorder/MappingContext { - public fun (Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation;Z)V - public synthetic fun (Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Z)V + public synthetic fun (Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation; - public final fun component2 ()Z - public final fun copy (Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation;Z)Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation;ZILjava/lang/Object;)Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext; + public final fun component2 ()Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper; + public final fun component3 ()Z + public final fun copy (Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Z)Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;ZILjava/lang/Object;)Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext; public fun equals (Ljava/lang/Object;)Z public final fun getHasOptionSelectorParent ()Z + public final fun getImageWireframeHelper ()Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper; public final fun getSystemInformation ()Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -75,16 +55,16 @@ public abstract interface class com/datadog/android/sessionreplay/internal/recor } public final class com/datadog/android/sessionreplay/internal/recorder/SystemInformation { - public fun (Lcom/datadog/android/sessionreplay/internal/recorder/GlobalBounds;IFLjava/lang/String;)V - public synthetic fun (Lcom/datadog/android/sessionreplay/internal/recorder/GlobalBounds;IFLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lcom/datadog/android/sessionreplay/internal/recorder/GlobalBounds; + public fun (Lcom/datadog/android/sessionreplay/utils/GlobalBounds;IFLjava/lang/String;)V + public synthetic fun (Lcom/datadog/android/sessionreplay/utils/GlobalBounds;IFLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/datadog/android/sessionreplay/utils/GlobalBounds; public final fun component2 ()I public final fun component3 ()F public final fun component4 ()Ljava/lang/String; - public final fun copy (Lcom/datadog/android/sessionreplay/internal/recorder/GlobalBounds;IFLjava/lang/String;)Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/internal/recorder/GlobalBounds;IFLjava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation; + public final fun copy (Lcom/datadog/android/sessionreplay/utils/GlobalBounds;IFLjava/lang/String;)Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/GlobalBounds;IFLjava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/internal/recorder/SystemInformation; public fun equals (Ljava/lang/Object;)Z - public final fun getScreenBounds ()Lcom/datadog/android/sessionreplay/internal/recorder/GlobalBounds; + public final fun getScreenBounds ()Lcom/datadog/android/sessionreplay/utils/GlobalBounds; public final fun getScreenDensity ()F public final fun getScreenOrientation ()I public final fun getThemeColor ()Ljava/lang/String; @@ -94,10 +74,7 @@ public final class com/datadog/android/sessionreplay/internal/recorder/SystemInf public abstract class com/datadog/android/sessionreplay/internal/recorder/mapper/BaseAsyncBackgroundWireframeMapper : com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper { public static final field Companion Lcom/datadog/android/sessionreplay/internal/recorder/mapper/BaseAsyncBackgroundWireframeMapper$Companion; - public fun ()V - public fun (Lcom/datadog/android/sessionreplay/utils/StringUtils;Lcom/datadog/android/sessionreplay/utils/ViewUtils;)V - public synthetic fun (Lcom/datadog/android/sessionreplay/utils/StringUtils;Lcom/datadog/android/sessionreplay/utils/ViewUtils;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/AsyncJobStatusCallback;)Ljava/util/List; + public fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;)Ljava/util/List; } public final class com/datadog/android/sessionreplay/internal/recorder/mapper/BaseAsyncBackgroundWireframeMapper$Companion { @@ -105,12 +82,12 @@ public final class com/datadog/android/sessionreplay/internal/recorder/mapper/Ba public abstract class com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper : com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper { public static final field Companion Lcom/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper$Companion; - public fun ()V - public fun (Lcom/datadog/android/sessionreplay/utils/StringUtils;Lcom/datadog/android/sessionreplay/utils/ViewUtils;)V - public synthetic fun (Lcom/datadog/android/sessionreplay/utils/StringUtils;Lcom/datadog/android/sessionreplay/utils/ViewUtils;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - protected final fun colorAndAlphaAsStringHexa (II)Ljava/lang/String; - protected final fun resolveShapeStyleAndBorder (Landroid/graphics/drawable/Drawable;F)Lkotlin/Pair; - protected final fun resolveViewGlobalBounds (Landroid/view/View;F)Lcom/datadog/android/sessionreplay/internal/recorder/GlobalBounds; + public fun (Lcom/datadog/android/sessionreplay/utils/ViewIdentifierResolver;Lcom/datadog/android/sessionreplay/utils/ColorStringFormatter;Lcom/datadog/android/sessionreplay/utils/ViewBoundsResolver;Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper;)V + protected final fun getColorStringFormatter ()Lcom/datadog/android/sessionreplay/utils/ColorStringFormatter; + protected final fun getDrawableToColorMapper ()Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper; + protected final fun getViewBoundsResolver ()Lcom/datadog/android/sessionreplay/utils/ViewBoundsResolver; + protected final fun getViewIdentifierResolver ()Lcom/datadog/android/sessionreplay/utils/ViewIdentifierResolver; + protected final fun resolveShapeStyle (Landroid/graphics/drawable/Drawable;F)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; protected final fun resolveViewId (Landroid/view/View;)J } @@ -118,28 +95,29 @@ public final class com/datadog/android/sessionreplay/internal/recorder/mapper/Ba } public class com/datadog/android/sessionreplay/internal/recorder/mapper/MaskInputTextViewMapper : com/datadog/android/sessionreplay/internal/recorder/mapper/TextViewMapper { - public fun ()V + public fun (Lcom/datadog/android/sessionreplay/utils/ViewIdentifierResolver;Lcom/datadog/android/sessionreplay/utils/ColorStringFormatter;Lcom/datadog/android/sessionreplay/utils/ViewBoundsResolver;Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper;)V } public class com/datadog/android/sessionreplay/internal/recorder/mapper/MaskTextViewMapper : com/datadog/android/sessionreplay/internal/recorder/mapper/TextViewMapper { - public fun ()V + public fun (Lcom/datadog/android/sessionreplay/utils/ViewIdentifierResolver;Lcom/datadog/android/sessionreplay/utils/ColorStringFormatter;Lcom/datadog/android/sessionreplay/utils/ViewBoundsResolver;Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper;)V } public class com/datadog/android/sessionreplay/internal/recorder/mapper/TextViewMapper : com/datadog/android/sessionreplay/internal/recorder/mapper/BaseAsyncBackgroundWireframeMapper { - public fun ()V - public synthetic fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/AsyncJobStatusCallback;)Ljava/util/List; - public fun map (Landroid/widget/TextView;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/AsyncJobStatusCallback;)Ljava/util/List; + public fun (Lcom/datadog/android/sessionreplay/utils/ViewIdentifierResolver;Lcom/datadog/android/sessionreplay/utils/ColorStringFormatter;Lcom/datadog/android/sessionreplay/utils/ViewBoundsResolver;Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper;)V + public synthetic fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;)Ljava/util/List; + public fun map (Landroid/widget/TextView;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;)Ljava/util/List; } public abstract interface class com/datadog/android/sessionreplay/internal/recorder/mapper/TraverseAllChildrenMapper : com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper { } public abstract interface class com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper { - public abstract fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/AsyncJobStatusCallback;)Ljava/util/List; + public abstract fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;)Ljava/util/List; } -public final class com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper$DefaultImpls { - public static synthetic fun map$default (Lcom/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper;Landroid/view/View;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/AsyncJobStatusCallback;ILjava/lang/Object;)Ljava/util/List; +public final class com/datadog/android/sessionreplay/internal/recorder/obfuscator/AndroidNStringObfuscator : com/datadog/android/sessionreplay/internal/recorder/obfuscator/StringObfuscator { + public fun ()V + public fun obfuscate (Ljava/lang/String;)Ljava/lang/String; } public final class com/datadog/android/sessionreplay/model/MobileSegment { @@ -1269,18 +1247,109 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$TextWireframeUpdate; } -public final class com/datadog/android/sessionreplay/utils/StringUtils { - public static final field INSTANCE Lcom/datadog/android/sessionreplay/utils/StringUtils; - public final fun formatColorAndAlphaAsHexa (II)Ljava/lang/String; +public class com/datadog/android/sessionreplay/utils/AndroidMDrawableToColorMapper : com/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapper { + public fun ()V + protected fun resolveInsetDrawable (Landroid/graphics/drawable/InsetDrawable;)Ljava/lang/Integer; + protected fun resolveRippleDrawable (Landroid/graphics/drawable/RippleDrawable;)Ljava/lang/Integer; +} + +public class com/datadog/android/sessionreplay/utils/AndroidQDrawableToColorMapper : com/datadog/android/sessionreplay/utils/AndroidMDrawableToColorMapper { + public static final field Companion Lcom/datadog/android/sessionreplay/utils/AndroidQDrawableToColorMapper$Companion; + public fun ()V + protected fun resolveGradientDrawable (Landroid/graphics/drawable/GradientDrawable;)Ljava/lang/Integer; +} + +public final class com/datadog/android/sessionreplay/utils/AndroidQDrawableToColorMapper$Companion { +} + +public abstract interface class com/datadog/android/sessionreplay/utils/AsyncJobStatusCallback { + public abstract fun jobFinished ()V + public abstract fun jobStarted ()V +} + +public abstract interface class com/datadog/android/sessionreplay/utils/ColorStringFormatter { + public abstract fun formatColorAndAlphaAsHexString (II)Ljava/lang/String; + public abstract fun formatColorAsHexString (I)Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/utils/DefaultColorStringFormatter : com/datadog/android/sessionreplay/utils/ColorStringFormatter { + public static final field INSTANCE Lcom/datadog/android/sessionreplay/utils/DefaultColorStringFormatter; + public fun formatColorAndAlphaAsHexString (II)Ljava/lang/String; + public fun formatColorAsHexString (I)Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/utils/DefaultViewBoundsResolver : com/datadog/android/sessionreplay/utils/ViewBoundsResolver { + public static final field INSTANCE Lcom/datadog/android/sessionreplay/utils/DefaultViewBoundsResolver; + public fun resolveViewGlobalBounds (Landroid/view/View;F)Lcom/datadog/android/sessionreplay/utils/GlobalBounds; +} + +public final class com/datadog/android/sessionreplay/utils/DefaultViewIdentifierResolver : com/datadog/android/sessionreplay/utils/ViewIdentifierResolver { + public static final field INSTANCE Lcom/datadog/android/sessionreplay/utils/DefaultViewIdentifierResolver; + public fun resolveChildUniqueIdentifier (Landroid/view/View;Ljava/lang/String;)Ljava/lang/Long; + public fun resolveViewId (Landroid/view/View;)J +} + +public abstract interface class com/datadog/android/sessionreplay/utils/DrawableToColorMapper { + public static final field Companion Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper$Companion; + public abstract fun mapDrawableToColor (Landroid/graphics/drawable/Drawable;)Ljava/lang/Integer; +} + +public final class com/datadog/android/sessionreplay/utils/DrawableToColorMapper$Companion { + public final fun getDefault ()Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper; +} + +public final class com/datadog/android/sessionreplay/utils/GlobalBounds { + public fun (JJJJ)V + public final fun component1 ()J + public final fun component2 ()J + public final fun component3 ()J + public final fun component4 ()J + public final fun copy (JJJJ)Lcom/datadog/android/sessionreplay/utils/GlobalBounds; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/utils/GlobalBounds;JJJJILjava/lang/Object;)Lcom/datadog/android/sessionreplay/utils/GlobalBounds; + public fun equals (Ljava/lang/Object;)Z + public final fun getHeight ()J + public final fun getWidth ()J + public final fun getX ()J + public final fun getY ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/datadog/android/sessionreplay/utils/ImageWireframeHelper { + public static final field Companion Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper$Companion; + public abstract fun createCompoundDrawableWireframes (Landroid/widget/TextView;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;ILcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;)Ljava/util/List; + public abstract fun createImageWireframe (Landroid/view/View;IJJIIZLandroid/graphics/drawable/Drawable;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; +} + +public final class com/datadog/android/sessionreplay/utils/ImageWireframeHelper$Companion { +} + +public final class com/datadog/android/sessionreplay/utils/ImageWireframeHelper$DefaultImpls { + public static synthetic fun createImageWireframe$default (Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Landroid/view/View;IJJIIZLandroid/graphics/drawable/Drawable;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; +} + +public class com/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapper : com/datadog/android/sessionreplay/utils/DrawableToColorMapper { + public static final field Companion Lcom/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapper$Companion; + public fun ()V + public fun mapDrawableToColor (Landroid/graphics/drawable/Drawable;)Ljava/lang/Integer; + protected final fun mergeColorAndAlpha (II)I + protected fun resolveColorDrawable (Landroid/graphics/drawable/ColorDrawable;)Ljava/lang/Integer; + protected fun resolveGradientDrawable (Landroid/graphics/drawable/GradientDrawable;)Ljava/lang/Integer; + protected fun resolveInsetDrawable (Landroid/graphics/drawable/InsetDrawable;)Ljava/lang/Integer; + protected fun resolveLayerDrawable (Landroid/graphics/drawable/LayerDrawable;Lkotlin/jvm/functions/Function2;)Ljava/lang/Integer; + public static synthetic fun resolveLayerDrawable$default (Lcom/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapper;Landroid/graphics/drawable/LayerDrawable;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/lang/Integer; + protected fun resolveRippleDrawable (Landroid/graphics/drawable/RippleDrawable;)Ljava/lang/Integer; +} + +public final class com/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapper$Companion { } -public final class com/datadog/android/sessionreplay/utils/UniqueIdentifierGenerator { - public static final field INSTANCE Lcom/datadog/android/sessionreplay/utils/UniqueIdentifierGenerator; - public final fun resolveChildUniqueIdentifier (Landroid/view/View;Ljava/lang/String;)Ljava/lang/Long; +public abstract interface class com/datadog/android/sessionreplay/utils/ViewBoundsResolver { + public abstract fun resolveViewGlobalBounds (Landroid/view/View;F)Lcom/datadog/android/sessionreplay/utils/GlobalBounds; } -public final class com/datadog/android/sessionreplay/utils/ViewUtils { - public static final field INSTANCE Lcom/datadog/android/sessionreplay/utils/ViewUtils; - public final fun resolveViewGlobalBounds (Landroid/view/View;F)Lcom/datadog/android/sessionreplay/internal/recorder/GlobalBounds; +public abstract interface class com/datadog/android/sessionreplay/utils/ViewIdentifierResolver { + public abstract fun resolveChildUniqueIdentifier (Landroid/view/View;Ljava/lang/String;)Ljava/lang/Long; + public abstract fun resolveViewId (Landroid/view/View;)J } diff --git a/features/dd-sdk-android-session-replay/build.gradle.kts b/features/dd-sdk-android-session-replay/build.gradle.kts index b995ef083f..732f002297 100644 --- a/features/dd-sdk-android-session-replay/build.gradle.kts +++ b/features/dd-sdk-android-session-replay/build.gradle.kts @@ -16,6 +16,7 @@ plugins { // Build id("com.android.library") kotlin("android") + id("com.google.devtools.ksp") // Publish `maven-publish` @@ -56,6 +57,8 @@ dependencies { implementation(libs.gson) implementation(libs.androidXAppCompat) + ksp(project(":tools:noopfactory")) + testImplementation(project(":tools:unit")) { attributes { attribute( @@ -74,6 +77,7 @@ dependencies { unMock { keep("android.widget.ImageView\$ScaleType") keep("android.graphics.Rect") + keep("android.graphics.drawable.GradientDrawable") } apply(from = "clone_session_replay_schema.gradle.kts") diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/NoOpRecorder.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/NoOpRecorder.kt deleted file mode 100644 index 25f2f9c96a..0000000000 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/NoOpRecorder.kt +++ /dev/null @@ -1,30 +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.sessionreplay - -internal class NoOpRecorder : Recorder { - - override fun unregisterCallbacks() { - // No Op - } - - override fun registerCallbacks() { - // No Op - } - - override fun resumeRecorders() { - // No Op - } - - override fun stopRecorders() { - // No Op - } - - override fun stopProcessingRecords() { - // No Op - } -} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacy.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacy.kt index 497f26602e..9d593cf024 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacy.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacy.kt @@ -18,8 +18,6 @@ import android.widget.SeekBar import android.widget.TextView import android.widget.Toolbar import androidx.appcompat.widget.SwitchCompat -import com.datadog.android.api.InternalLogger -import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler import com.datadog.android.sessionreplay.internal.recorder.mapper.BasePickerMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.ButtonMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckBoxMapper @@ -41,11 +39,15 @@ import com.datadog.android.sessionreplay.internal.recorder.mapper.SwitchCompatMa import com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.UnsupportedViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper -import com.datadog.android.sessionreplay.internal.recorder.resources.BitmapPool -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper -import com.datadog.android.sessionreplay.internal.recorder.resources.ResourcesLRUCache -import com.datadog.android.sessionreplay.internal.recorder.resources.ResourcesSerializer -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator +import com.datadog.android.sessionreplay.internal.recorder.obfuscator.rules.AllowObfuscationRule +import com.datadog.android.sessionreplay.internal.utils.ImageViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter +import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver +import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import androidx.appcompat.widget.Toolbar as AppCompatToolbar /** @@ -77,24 +79,28 @@ enum class SessionReplayPrivacy { */ MASK_USER_INPUT; - @Suppress("LongMethod") - internal fun mappers( - internalLogger: InternalLogger, - applicationId: String, - recordedDataQueueHandler: RecordedDataQueueHandler - ): List { - val resourcesSerializer = buildResourcesSerializer(applicationId, recordedDataQueueHandler) - val imageWireframeHelper = ImageWireframeHelper( - logger = internalLogger, - resourcesSerializer = resourcesSerializer - ) - val uniqueIdentifierGenerator = UniqueIdentifierGenerator + private val viewIdentifierResolver: ViewIdentifierResolver = DefaultViewIdentifierResolver + private val colorStringFormatter: ColorStringFormatter = DefaultColorStringFormatter + private val viewBoundsResolver: ViewBoundsResolver = DefaultViewBoundsResolver + private val drawableToColorMapper: DrawableToColorMapper = DrawableToColorMapper.getDefault() - val unsupportedViewMapper = UnsupportedViewMapper() - val imageViewMapper = ImageViewMapper( - imageWireframeHelper = imageWireframeHelper, - uniqueIdentifierGenerator = uniqueIdentifierGenerator - ) + @Suppress("LongMethod") + internal fun mappers(): List { + val unsupportedViewMapper = + UnsupportedViewMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + val imageViewMapper = + ImageViewMapper( + ImageViewUtils, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) val textMapper: TextViewMapper val buttonMapper: ButtonMapper val checkedTextViewMapper: CheckedTextViewMapper @@ -106,40 +112,121 @@ enum class SessionReplayPrivacy { when (this) { ALLOW -> { textMapper = TextViewMapper( - imageWireframeHelper = imageWireframeHelper, - uniqueIdentifierGenerator = uniqueIdentifierGenerator + AllowObfuscationRule(), + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper ) buttonMapper = ButtonMapper(textMapper) - checkedTextViewMapper = CheckedTextViewMapper(textMapper) - checkBoxMapper = CheckBoxMapper(textMapper) - radioButtonMapper = RadioButtonMapper(textMapper) - switchCompatMapper = SwitchCompatMapper(textMapper) + checkedTextViewMapper = CheckedTextViewMapper( + textMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + checkBoxMapper = CheckBoxMapper( + textMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + radioButtonMapper = RadioButtonMapper( + textMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + switchCompatMapper = SwitchCompatMapper( + textMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) seekBarMapper = getSeekBarMapper() numberPickerMapper = getNumberPickerMapper() } + MASK -> { textMapper = MaskTextViewMapper( - imageWireframeHelper = imageWireframeHelper, - uniqueIdentifierGenerator = uniqueIdentifierGenerator + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper ) buttonMapper = ButtonMapper(textMapper) - checkedTextViewMapper = MaskCheckedTextViewMapper(textMapper) - checkBoxMapper = MaskCheckBoxMapper(textMapper) - radioButtonMapper = MaskRadioButtonMapper(textMapper) - switchCompatMapper = MaskSwitchCompatMapper(textMapper) + checkedTextViewMapper = MaskCheckedTextViewMapper( + textMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + checkBoxMapper = MaskCheckBoxMapper( + textMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + radioButtonMapper = MaskRadioButtonMapper( + textMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + switchCompatMapper = MaskSwitchCompatMapper( + textMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) seekBarMapper = getMaskSeekBarMapper() numberPickerMapper = getMaskNumberPickerMapper() } + MASK_USER_INPUT -> { textMapper = MaskInputTextViewMapper( - imageWireframeHelper = imageWireframeHelper, - uniqueIdentifierGenerator = uniqueIdentifierGenerator + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper ) buttonMapper = ButtonMapper(textMapper) - checkedTextViewMapper = MaskCheckedTextViewMapper(textMapper) - checkBoxMapper = MaskCheckBoxMapper(textMapper) - radioButtonMapper = MaskRadioButtonMapper(textMapper) - switchCompatMapper = MaskSwitchCompatMapper(textMapper) + checkedTextViewMapper = MaskCheckedTextViewMapper( + textMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + checkBoxMapper = MaskCheckBoxMapper( + textMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + radioButtonMapper = MaskRadioButtonMapper( + textMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + switchCompatMapper = MaskSwitchCompatMapper( + textMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) seekBarMapper = getMaskSeekBarMapper() numberPickerMapper = getMaskNumberPickerMapper() } @@ -175,26 +262,14 @@ enum class SessionReplayPrivacy { return mappersList } - private fun buildResourcesSerializer( - applicationId: String, - recordedDataQueueHandler: - RecordedDataQueueHandler - ): ResourcesSerializer { - val bitmapPool = BitmapPool() - val resourcesLRUCache = ResourcesLRUCache() - - val builder = ResourcesSerializer.Builder( - applicationId = applicationId, - recordedDataQueueHandler = recordedDataQueueHandler, - bitmapPool = bitmapPool, - resourcesLRUCache = resourcesLRUCache - ) - return builder.build() - } - private fun getMaskSeekBarMapper(): MaskSeekBarWireframeMapper? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - MaskSeekBarWireframeMapper() + MaskSeekBarWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) } else { null } @@ -202,7 +277,12 @@ enum class SessionReplayPrivacy { private fun getSeekBarMapper(): SeekBarWireframeMapper? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - SeekBarWireframeMapper() + SeekBarWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) } else { null } @@ -210,7 +290,12 @@ enum class SessionReplayPrivacy { private fun getNumberPickerMapper(): BasePickerMapper? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - NumberPickerMapper() + NumberPickerMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) } else { null } @@ -218,7 +303,12 @@ enum class SessionReplayPrivacy { private fun getMaskNumberPickerMapper(): BasePickerMapper? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MaskNumberPickerMapper() + MaskNumberPickerMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) } else { null } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayRecorder.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayRecorder.kt index 5e35f77572..22f17bb84d 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayRecorder.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayRecorder.kt @@ -25,14 +25,33 @@ import com.datadog.android.sessionreplay.internal.recorder.OptionSelectorDetecto import com.datadog.android.sessionreplay.internal.recorder.SnapshotProducer import com.datadog.android.sessionreplay.internal.recorder.TreeViewTraversal import com.datadog.android.sessionreplay.internal.recorder.ViewOnDrawInterceptor +import com.datadog.android.sessionreplay.internal.recorder.ViewUtilsInternal import com.datadog.android.sessionreplay.internal.recorder.WindowCallbackInterceptor import com.datadog.android.sessionreplay.internal.recorder.WindowInspector import com.datadog.android.sessionreplay.internal.recorder.callback.OnWindowRefreshedCallback +import com.datadog.android.sessionreplay.internal.recorder.mapper.DecorViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.MapperTypeWrapper +import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewWireframeMapper +import com.datadog.android.sessionreplay.internal.recorder.resources.BitmapCachesManager +import com.datadog.android.sessionreplay.internal.recorder.resources.BitmapPool +import com.datadog.android.sessionreplay.internal.recorder.resources.DefaultImageWireframeHelper +import com.datadog.android.sessionreplay.internal.recorder.resources.ImageTypeResolver +import com.datadog.android.sessionreplay.internal.recorder.resources.MD5HashGenerator +import com.datadog.android.sessionreplay.internal.recorder.resources.ResourceResolver +import com.datadog.android.sessionreplay.internal.recorder.resources.ResourcesLRUCache +import com.datadog.android.sessionreplay.internal.recorder.resources.WebPImageCompression import com.datadog.android.sessionreplay.internal.storage.RecordWriter import com.datadog.android.sessionreplay.internal.storage.ResourcesWriter +import com.datadog.android.sessionreplay.internal.utils.DrawableUtils import com.datadog.android.sessionreplay.internal.utils.RumContextProvider import com.datadog.android.sessionreplay.internal.utils.TimeProvider +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter +import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver +import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder { @@ -94,15 +113,49 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder { internalLogger = internalLogger ) + val viewIdentifierResolver: ViewIdentifierResolver = DefaultViewIdentifierResolver + val colorStringFormatter: ColorStringFormatter = DefaultColorStringFormatter + val viewBoundsResolver: ViewBoundsResolver = DefaultViewBoundsResolver + val drawableToColorMapper: DrawableToColorMapper = DrawableToColorMapper.getDefault() + + val defaultVWM = ViewWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) + + val bitmapCachesManager = BitmapCachesManager( + bitmapPool = BitmapPool(), + resourcesLRUCache = ResourcesLRUCache(), + logger = internalLogger + ) + + val resourceResolver = ResourceResolver( + applicationId = applicationId, + recordedDataQueueHandler = recordedDataQueueHandler, + bitmapCachesManager = bitmapCachesManager, + drawableUtils = DrawableUtils(internalLogger, bitmapCachesManager), + logger = internalLogger, + md5HashGenerator = MD5HashGenerator(internalLogger), + webPImageCompression = WebPImageCompression(internalLogger) + ) + this.viewOnDrawInterceptor = ViewOnDrawInterceptor( recordedDataQueueHandler = recordedDataQueueHandler, SnapshotProducer( + DefaultImageWireframeHelper( + logger = internalLogger, + resourceResolver = resourceResolver, + viewIdentifierResolver = viewIdentifierResolver, + viewUtilsInternal = ViewUtilsInternal(), + imageTypeResolver = ImageTypeResolver() + ), TreeViewTraversal( - customMappers + privacy.mappers( - internalLogger, - applicationId, - recordedDataQueueHandler - ) + mappers = customMappers + privacy.mappers(), + viewMapper = defaultVWM, + decorViewMapper = DecorViewMapper(defaultVWM, viewIdentifierResolver), + viewUtilsInternal = ViewUtilsInternal() ), ComposedOptionSelectorDetector( customOptionSelectorDetectors + DefaultOptionSelectorDetector() diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/NoOpAsyncJobStatusCallback.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/NoOpAsyncJobStatusCallback.kt deleted file mode 100644 index a4faccabf8..0000000000 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/NoOpAsyncJobStatusCallback.kt +++ /dev/null @@ -1,16 +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.sessionreplay.internal - -internal class NoOpAsyncJobStatusCallback : AsyncJobStatusCallback { - override fun jobStarted() { - // No-op - } - override fun jobFinished() { - // No-op - } -} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/ResourceRequestBodyFactory.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/ResourceRequestBodyFactory.kt index fd9cff3e76..acb2428e24 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/ResourceRequestBodyFactory.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/ResourceRequestBodyFactory.kt @@ -206,7 +206,7 @@ internal class ResourceRequestBodyFactory( internal const val MULTIPLE_APPLICATION_ID_ERROR = "There were multiple applicationIds associated with the resources" internal const val NO_RESOURCES_TO_SEND_ERROR = - "No resources to send" + "No resources to send" private const val ERROR_CREATING_REQUEST_BODY = "Error creating request body" private const val EMPTY_REQUEST_BODY_ERROR = diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/MappingContext.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/MappingContext.kt index cc615d3cdb..f7c0be338f 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/MappingContext.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/MappingContext.kt @@ -6,14 +6,19 @@ package com.datadog.android.sessionreplay.internal.recorder +import com.datadog.android.sessionreplay.utils.ImageWireframeHelper + /** * Contains the context information which will be passed from parent to its children when - * traversing the tree view for masking. + * traversing the tree view for masking, as well as utilities and helpers that allow generating the wireframes + * expected by Datadog. * @param systemInformation as [SystemInformation] + * @param imageWireframeHelper a helper tool to capture images within a View * @param hasOptionSelectorParent tells if one of the parents of the current [android.view.View] * is an option selector type (e.g. time picker, date picker, drop - down list) */ data class MappingContext( val systemInformation: SystemInformation, + val imageWireframeHelper: ImageWireframeHelper, val hasOptionSelectorParent: Boolean = false ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducer.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducer.kt index 8a213cc2e2..030381c5e2 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducer.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducer.kt @@ -10,9 +10,11 @@ import android.view.View import android.view.ViewGroup import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.utils.ImageWireframeHelper import java.util.LinkedList internal class SnapshotProducer( + private val imageWireframeHelper: ImageWireframeHelper, private val treeViewTraversal: TreeViewTraversal, private val optionSelectorDetector: OptionSelectorDetector = ComposedOptionSelectorDetector(listOf(DefaultOptionSelectorDetector())) @@ -25,7 +27,7 @@ internal class SnapshotProducer( ): Node? { return convertViewToNode( rootView, - MappingContext(systemInformation), + MappingContext(systemInformation, imageWireframeHelper), LinkedList(), recordedDataQueueRefs ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SystemInformation.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SystemInformation.kt index 7fb3b36c30..6b111f5461 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SystemInformation.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SystemInformation.kt @@ -7,11 +7,12 @@ package com.datadog.android.sessionreplay.internal.recorder import android.content.res.Configuration +import com.datadog.android.sessionreplay.utils.GlobalBounds /** * Provides information about the current system. - * @param screenBounds as screen bounds in Global coordinates - * @param screenOrientation as current screen orientation + * @param screenBounds the screen bounds in Global coordinates + * @param screenOrientation the current screen orientation * @param screenDensity current screen density * @param themeColor application theme color */ diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversal.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversal.kt index 7fc9b960f1..a54573c787 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversal.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversal.kt @@ -15,12 +15,13 @@ import com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChi import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewWireframeMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.utils.NoOpAsyncJobStatusCallback internal class TreeViewTraversal( private val mappers: List, - private val viewMapper: ViewWireframeMapper = ViewWireframeMapper(), - private val decorViewMapper: DecorViewMapper = DecorViewMapper(viewMapper), - private val viewUtilsInternal: ViewUtilsInternal = ViewUtilsInternal() + private val viewMapper: ViewWireframeMapper, + private val decorViewMapper: DecorViewMapper, + private val viewUtilsInternal: ViewUtilsInternal ) { @Suppress("ReturnCount") @@ -52,10 +53,10 @@ internal class TreeViewTraversal( resolvedWireframes = queueableViewMapper.map(view, mappingContext) } else if (isDecorView(view)) { traversalStrategy = TraversalStrategy.TRAVERSE_ALL_CHILDREN - resolvedWireframes = decorViewMapper.map(view, mappingContext) + resolvedWireframes = decorViewMapper.map(view, mappingContext, NoOpAsyncJobStatusCallback()) } else { traversalStrategy = TraversalStrategy.TRAVERSE_ALL_CHILDREN - resolvedWireframes = viewMapper.map(view, mappingContext) + resolvedWireframes = viewMapper.map(view, mappingContext, NoOpAsyncJobStatusCallback()) } return TraversedTreeView(resolvedWireframes, traversalStrategy) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewUtilsInternal.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewUtilsInternal.kt index 5bd31743e0..f4eae514f2 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewUtilsInternal.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewUtilsInternal.kt @@ -11,7 +11,8 @@ import android.view.View import android.view.ViewStub import androidx.appcompat.widget.ActionBarContextView import androidx.appcompat.widget.Toolbar -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper +import com.datadog.android.sessionreplay.internal.recorder.resources.DefaultImageWireframeHelper +import com.datadog.android.sessionreplay.utils.GlobalBounds internal class ViewUtilsInternal { @@ -24,9 +25,7 @@ internal class ViewUtilsInternal { } internal fun isSystemNoise(view: View): Boolean { - return view.id in systemViewIds || - view is ViewStub || - view is ActionBarContextView + return view.id in systemViewIds || view is ViewStub || view is ActionBarContextView } @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here @@ -55,7 +54,7 @@ internal class ViewUtilsInternal { view: View, drawable: Drawable, pixelsDensity: Float, - position: ImageWireframeHelper.CompoundDrawablePositions + position: DefaultImageWireframeHelper.CompoundDrawablePositions ): GlobalBounds { val coordinates = IntArray(2) // this will always have size >= 2 @@ -76,19 +75,22 @@ internal class ViewUtilsInternal { var yPosition: Long when (position) { - ImageWireframeHelper.CompoundDrawablePositions.LEFT -> { + DefaultImageWireframeHelper.CompoundDrawablePositions.LEFT -> { xPosition = viewPaddingStart yPosition = getCenterVerticalOffset(viewHeight, drawableHeight) } - ImageWireframeHelper.CompoundDrawablePositions.TOP -> { + + DefaultImageWireframeHelper.CompoundDrawablePositions.TOP -> { xPosition = getCenterHorizontalOffset(viewWidth, drawableWidth) yPosition = viewPaddingTop } - ImageWireframeHelper.CompoundDrawablePositions.RIGHT -> { + + DefaultImageWireframeHelper.CompoundDrawablePositions.RIGHT -> { xPosition = viewWidth - (drawableWidth + viewPaddingEnd) yPosition = getCenterVerticalOffset(viewHeight, drawableHeight) } - ImageWireframeHelper.CompoundDrawablePositions.BOTTOM -> { + + DefaultImageWireframeHelper.CompoundDrawablePositions.BOTTOM -> { xPosition = getCenterHorizontalOffset(viewWidth, drawableWidth) yPosition = viewHeight - (drawableHeight + viewPaddingBottom) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseAsyncBackgroundWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseAsyncBackgroundWireframeMapper.kt index f3c3be0755..ef4844e855 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseAsyncBackgroundWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseAsyncBackgroundWireframeMapper.kt @@ -7,35 +7,31 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.view.View -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.densityNormalized -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelperCallback import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver @Suppress("UndocumentedPublicClass") -abstract class BaseAsyncBackgroundWireframeMapper( - stringUtils: StringUtils = StringUtils, - viewUtils: ViewUtils = ViewUtils -) : BaseWireframeMapper(stringUtils, viewUtils) { - - // Why is this nullable ??? - // TODO: RUM-0000 Make the ImageWireframeHelper non nullable - private var imageWireframeHelper: ImageWireframeHelper? = null - private var uniqueIdentifierGenerator = UniqueIdentifierGenerator - - internal constructor( - imageWireframeHelper: ImageWireframeHelper, - uniqueIdentifierGenerator: UniqueIdentifierGenerator - ) : this() { - this.imageWireframeHelper = imageWireframeHelper - this.uniqueIdentifierGenerator = uniqueIdentifierGenerator - } +abstract class BaseAsyncBackgroundWireframeMapper internal constructor( + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : BaseWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) { + + private var uniqueIdentifierGenerator = DefaultViewIdentifierResolver /** * Maps the [View] into a list of [MobileSegment.Wireframe]. @@ -45,34 +41,31 @@ abstract class BaseAsyncBackgroundWireframeMapper( mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback ): List { - val wireframes = mutableListOf() - - resolveViewBackground(view, asyncJobStatusCallback)?.let { - wireframes.add(it) - } + val backgroundWireframe = resolveViewBackground(view, mappingContext, asyncJobStatusCallback) - return wireframes + return backgroundWireframe?.let { listOf(it) } ?: emptyList() } private fun resolveViewBackground( view: View, + mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback ): MobileSegment.Wireframe? { - val (shapeStyle, border) = view.background?.resolveShapeStyleAndBorder(view.alpha) - ?: (null to null) + val shapeStyle = view.background?.let { resolveShapeStyle(it, view.alpha) } val resources = view.resources val density = resources.displayMetrics.density - val bounds = resolveViewGlobalBounds(view, density) + val bounds = viewBoundsResolver.resolveViewGlobalBounds(view, density) val width = view.width val height = view.height - return if (border == null && shapeStyle == null) { + return if (shapeStyle == null) { resolveBackgroundAsImageWireframe( view = view, bounds = bounds, width = width, height = height, + mappingContext = mappingContext, asyncJobStatusCallback = asyncJobStatusCallback ) } else { @@ -81,8 +74,7 @@ abstract class BaseAsyncBackgroundWireframeMapper( bounds = bounds, width = width, height = height, - shapeStyle = shapeStyle, - border = border + shapeStyle = shapeStyle ) } } @@ -92,8 +84,7 @@ abstract class BaseAsyncBackgroundWireframeMapper( bounds: GlobalBounds, width: Int, height: Int, - shapeStyle: MobileSegment.ShapeStyle?, - border: MobileSegment.ShapeBorder? + shapeStyle: MobileSegment.ShapeStyle? ): MobileSegment.Wireframe.ShapeWireframe? { val id = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( view, @@ -109,7 +100,7 @@ abstract class BaseAsyncBackgroundWireframeMapper( width = width.densityNormalized(density).toLong(), height = height.densityNormalized(density).toLong(), shapeStyle = shapeStyle, - border = border + border = null ) } @@ -118,38 +109,35 @@ abstract class BaseAsyncBackgroundWireframeMapper( bounds: GlobalBounds, width: Int, height: Int, + mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback ): MobileSegment.Wireframe? { val resources = view.resources val drawableCopy = view.background?.constantState?.newDrawable(resources) - @Suppress("ThreadSafety") // TODO REPLAY-1861 caller thread of .map is unknown? - return imageWireframeHelper?.createImageWireframe( - view = view, - 0, - x = bounds.x, - y = bounds.y, - width, - height, - clipping = MobileSegment.WireframeClip(), - drawable = drawableCopy, - shapeStyle = null, - border = null, - prefix = PREFIX_BACKGROUND_DRAWABLE, - usePIIPlaceholder = false, - imageWireframeHelperCallback = object : ImageWireframeHelperCallback { - override fun onFinished() { - asyncJobStatusCallback.jobFinished() - } - - override fun onStart() { - asyncJobStatusCallback.jobStarted() - } - } - ) + return if (drawableCopy != null) { + @Suppress("ThreadSafety") // TODO REPLAY-1861 caller thread of .map is unknown? + mappingContext.imageWireframeHelper.createImageWireframe( + view = view, + currentWireframeIndex = 0, + x = bounds.x, + y = bounds.y, + width = width, + height = height, + usePIIPlaceholder = false, + drawable = drawableCopy, + asyncJobStatusCallback = asyncJobStatusCallback, + clipping = MobileSegment.WireframeClip(), + shapeStyle = null, + border = null, + prefix = PREFIX_BACKGROUND_DRAWABLE + ) + } else { + null + } } companion object { - private const val PREFIX_BACKGROUND_DRAWABLE = "backgroundDrawable" + internal const val PREFIX_BACKGROUND_DRAWABLE = "backgroundDrawable" } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BasePickerMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BasePickerMapper.kt index 0fd93b1603..74eeec5234 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BasePickerMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BasePickerMapper.kt @@ -9,17 +9,26 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.os.Build import android.widget.NumberPicker import androidx.annotation.RequiresApi -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver @RequiresApi(Build.VERSION_CODES.Q) internal abstract class BasePickerMapper( - stringUtils: StringUtils = StringUtils, - viewUtils: ViewUtils = ViewUtils -) : BaseWireframeMapper(stringUtils, viewUtils) { + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : BaseWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) { protected fun resolveTextSize(view: NumberPicker, screenDensity: Float): Long { return view.textSize.toLong().densityNormalized(screenDensity) @@ -32,9 +41,11 @@ internal abstract class BasePickerMapper( protected fun resolveDividerPaddingStart(view: NumberPicker, screenDensity: Float): Long { return view.paddingStart.toLong().densityNormalized(screenDensity) } + protected fun resolveDividerPaddingEnd(view: NumberPicker, screenDensity: Float): Long { return view.paddingEnd.toLong().densityNormalized(screenDensity) } + protected fun resolveDividerHeight(screenDensity: Float): Long { return DIVIDER_HEIGHT_IN_PX.densityNormalized(screenDensity) } @@ -47,7 +58,7 @@ internal abstract class BasePickerMapper( } protected fun resolveSelectedTextColor(view: NumberPicker): String { - return colorAndAlphaAsStringHexa(view.textColor, OPAQUE_ALPHA_VALUE) + return colorStringFormatter.formatColorAndAlphaAsHexString(view.textColor, OPAQUE_ALPHA_VALUE) } @Suppress("LongParameterList") diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper.kt index b9db095371..70181feda9 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper.kt @@ -6,73 +6,38 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper -import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable -import android.graphics.drawable.InsetDrawable -import android.graphics.drawable.RippleDrawable -import android.os.Build import android.view.View -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds -import com.datadog.android.sessionreplay.internal.recorder.safeGetDrawable import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver @Suppress("UndocumentedPublicClass") abstract class BaseWireframeMapper( - private val stringUtils: StringUtils = StringUtils, - private val viewUtils: ViewUtils = ViewUtils + protected val viewIdentifierResolver: ViewIdentifierResolver, + protected val colorStringFormatter: ColorStringFormatter, + protected val viewBoundsResolver: ViewBoundsResolver, + protected val drawableToColorMapper: DrawableToColorMapper ) : WireframeMapper { /** * Resolves the [View] unique id to be used in the mapped [MobileSegment.Wireframe]. */ protected fun resolveViewId(view: View): Long { - // we will use the System.identityHashcode in here which always returns the default - // hashcode value whether or not a child class overrides this. - return System.identityHashCode(view).toLong() + return viewIdentifierResolver.resolveViewId(view) } /** - * Takes color and the alpha value and returns a string formatted color in RGBA format - * (e.g. #000000FF). + * Resolves the [MobileSegment.ShapeStyle] based on the [View] drawables. */ - protected fun colorAndAlphaAsStringHexa(color: Int, alphaAsHexa: Int): String { - return stringUtils.formatColorAndAlphaAsHexa(color, alphaAsHexa) - } - - /** - * Resolves the [View] bounds. These dimensions are already normalized according with - * the provided [pixelsDensity]. By Global we mean that the View position will not be relative - * to its parent but to the Device screen. - */ - protected fun resolveViewGlobalBounds(view: View, pixelsDensity: Float): GlobalBounds { - // RUMM-0000 return an array of primitives here instead of creating an object. - // This method is being called too often every time we take a screen snapshot - // and we might want to avoid creating too many instances. - return viewUtils.resolveViewGlobalBounds(view, pixelsDensity) - } - - /** - * Resolves the [MobileSegment.ShapeStyle] and [MobileSegment.ShapeBorder] based on the [View] - * drawables. - */ - protected fun Drawable.resolveShapeStyleAndBorder( - viewAlpha: Float - ): Pair? { - return if (this is ColorDrawable) { - val color = colorAndAlphaAsStringHexa(color, alpha) - MobileSegment.ShapeStyle(color, viewAlpha) to null - } else if (this is RippleDrawable && numberOfLayers >= 1) { - this.safeGetDrawable(0)?.resolveShapeStyleAndBorder(viewAlpha) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && this is InsetDrawable) { - drawable?.resolveShapeStyleAndBorder(viewAlpha) + protected fun resolveShapeStyle(drawable: Drawable, viewAlpha: Float): MobileSegment.ShapeStyle? { + val color = drawableToColorMapper.mapDrawableToColor(drawable) + return if (color != null) { + MobileSegment.ShapeStyle(colorStringFormatter.formatColorAsHexString(color), viewAlpha) } else { - // We cannot handle this drawable so we will use a border to delimit its container - // bounds. - // TODO: RUMM-0000 In case the background drawable could not be handled we should - // instead resolve it as an ImageWireframe. - null to null + null } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ButtonMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ButtonMapper.kt index 5ccf901333..ce1fdfbbec 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ButtonMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ButtonMapper.kt @@ -7,14 +7,13 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.Button -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback internal class ButtonMapper( - private val textWireframeMapper: TextViewMapper = TextViewMapper() -) : - WireframeMapper { + private val textWireframeMapper: TextViewMapper +) : WireframeMapper { override fun map( view: Button, mappingContext: MappingContext, diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckBoxMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckBoxMapper.kt index 8c7695f7a2..bfe11602d8 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckBoxMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckBoxMapper.kt @@ -7,18 +7,21 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.CheckBox -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal open class CheckBoxMapper( textWireframeMapper: TextViewMapper, - stringUtils: StringUtils = StringUtils, - uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator, - viewUtils: ViewUtils = ViewUtils + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper ) : CheckableCompoundButtonMapper( textWireframeMapper, - stringUtils, - uniqueIdentifierGenerator, - viewUtils + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableCompoundButtonMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableCompoundButtonMapper.kt index 5b8f722814..623436fe35 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableCompoundButtonMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableCompoundButtonMapper.kt @@ -8,28 +8,31 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.os.Build import android.widget.CompoundButton -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.densityNormalized -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal abstract class CheckableCompoundButtonMapper( textWireframeMapper: TextViewMapper, - stringUtils: StringUtils = StringUtils, - uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator, - viewUtils: ViewUtils = ViewUtils + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper ) : CheckableTextViewMapper( textWireframeMapper, - stringUtils, - uniqueIdentifierGenerator, - viewUtils + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper ) { // region CheckableTextViewMapper override fun resolveCheckableBounds(view: T, pixelsDensity: Float): GlobalBounds { - val viewGlobalBounds = resolveViewGlobalBounds(view, pixelsDensity) + val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds(view, pixelsDensity) var checkBoxHeight = DEFAULT_CHECKABLE_HEIGHT_IN_PX if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { view.buttonDrawable?.let { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableTextViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableTextViewMapper.kt index 8a21b1f2dc..92c497ad84 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableTextViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableTextViewMapper.kt @@ -8,20 +8,27 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.Checkable import android.widget.TextView -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal abstract class CheckableTextViewMapper( private val textWireframeMapper: TextViewMapper, - private val stringUtils: StringUtils = StringUtils, - private val uniqueIdentifierGenerator: UniqueIdentifierGenerator, - viewUtils: ViewUtils = ViewUtils -) : CheckableWireframeMapper(viewUtils) where T : TextView, T : Checkable { + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : CheckableWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) where T : TextView, T : Checkable { // region CheckableWireframeMapper @@ -37,7 +44,7 @@ internal abstract class CheckableTextViewMapper( view: T, mappingContext: MappingContext ): List? { - val checkableId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + val checkableId = viewIdentifierResolver.resolveChildUniqueIdentifier( view, CHECKABLE_KEY_NAME ) ?: return null @@ -65,7 +72,7 @@ internal abstract class CheckableTextViewMapper( view: T, mappingContext: MappingContext ): List? { - val checkableId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + val checkableId = viewIdentifierResolver.resolveChildUniqueIdentifier( view, CHECKABLE_KEY_NAME ) ?: return null @@ -96,7 +103,7 @@ internal abstract class CheckableTextViewMapper( abstract fun resolveCheckableBounds(view: T, pixelsDensity: Float): GlobalBounds protected open fun resolveCheckableColor(view: T): String { - return stringUtils.formatColorAndAlphaAsHexa(view.currentTextColor, OPAQUE_ALPHA_VALUE) + return colorStringFormatter.formatColorAndAlphaAsHexString(view.currentTextColor, OPAQUE_ALPHA_VALUE) } protected open fun resolveCheckedShapeStyle(view: T, checkBoxColor: String): MobileSegment.ShapeStyle? { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableWireframeMapper.kt index bc04c76d49..03eaa8ff9d 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableWireframeMapper.kt @@ -8,14 +8,25 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.view.View import android.widget.Checkable -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver -internal abstract class CheckableWireframeMapper(viewUtils: ViewUtils) : - BaseWireframeMapper(viewUtils = viewUtils) - where T : View, T : Checkable { +internal abstract class CheckableWireframeMapper( + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : BaseWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) where T : View, T : Checkable { override fun map( view: T, diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckedTextViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckedTextViewMapper.kt index c0a68a841b..fca94d5914 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckedTextViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckedTextViewMapper.kt @@ -7,38 +7,36 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.CheckedTextView -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.densityNormalized -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal open class CheckedTextViewMapper( textWireframeMapper: TextViewMapper, - private val stringUtils: StringUtils = StringUtils, - uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator, - viewUtils: ViewUtils = ViewUtils + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper ) : CheckableTextViewMapper( textWireframeMapper, - stringUtils, - uniqueIdentifierGenerator, - viewUtils + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper ) { // region CheckableTextViewMapper override fun resolveCheckableColor(view: CheckedTextView): String { - view.checkMarkTintList?.let { - return stringUtils.formatColorAndAlphaAsHexa( - it.defaultColor, - OPAQUE_ALPHA_VALUE - ) - } - return stringUtils.formatColorAndAlphaAsHexa(view.currentTextColor, OPAQUE_ALPHA_VALUE) + val color = view.checkMarkTintList?.defaultColor ?: view.currentTextColor + return colorStringFormatter.formatColorAndAlphaAsHexString(color, OPAQUE_ALPHA_VALUE) } override fun resolveCheckableBounds(view: CheckedTextView, pixelsDensity: Float): GlobalBounds { - val viewGlobalBounds = resolveViewGlobalBounds(view, pixelsDensity) + val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds(view, pixelsDensity) val textViewPaddingRight = view.totalPaddingRight.toLong().densityNormalized(pixelsDensity) var checkBoxHeight = 0L diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/DecorViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/DecorViewMapper.kt index 92d79d1124..4b5a127c1a 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/DecorViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/DecorViewMapper.kt @@ -7,15 +7,17 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.view.View -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver +import com.datadog.android.sessionreplay.utils.NoOpAsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import java.util.Locale internal class DecorViewMapper( private val viewWireframeMapper: ViewWireframeMapper, - private val uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator + private val viewIdentifierResolver: ViewIdentifierResolver = DefaultViewIdentifierResolver ) : WireframeMapper { override fun map( @@ -23,7 +25,7 @@ internal class DecorViewMapper( mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback ): List { - val wireframes = viewWireframeMapper.map(view, mappingContext) + val wireframes = viewWireframeMapper.map(view, mappingContext, NoOpAsyncJobStatusCallback()) .toMutableList() if (mappingContext.systemInformation.themeColor != null) { // we add the background color from the theme to the decorView @@ -44,9 +46,9 @@ internal class DecorViewMapper( // we could find. We know that the system classes are not obfuscated by Proguard so in case // this class name will be changed in the future we will see it in our replays and fix it. if (!decorClassName.lowercase(Locale.US) - .endsWith(POP_UP_DECOR_VIEW_CLASS_NAME_SUFFIX) + .endsWith(POP_UP_DECOR_VIEW_CLASS_NAME_SUFFIX) ) { - uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + viewIdentifierResolver.resolveChildUniqueIdentifier( view, WINDOW_KEY_NAME )?.let { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapper.kt index e6821b65d5..420efa33d0 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapper.kt @@ -7,22 +7,28 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.ImageView -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.densityNormalized -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelperCallback import com.datadog.android.sessionreplay.internal.utils.ImageViewUtils import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ImageWireframeHelper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal class ImageViewMapper( - private val imageWireframeHelper: ImageWireframeHelper, - private val imageViewUtils: ImageViewUtils = ImageViewUtils, - uniqueIdentifierGenerator: UniqueIdentifierGenerator + private val imageViewUtils: ImageViewUtils, + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper ) : BaseAsyncBackgroundWireframeMapper( - imageWireframeHelper = imageWireframeHelper, - uniqueIdentifierGenerator = uniqueIdentifierGenerator + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper ) { override fun map( view: ImageView, @@ -48,32 +54,26 @@ internal class ImageViewMapper( val contentHeightPx = contentRect.height() val contentDrawable = drawable.constantState?.newDrawable(resources) - // resolve foreground - @Suppress("ThreadSafety") // TODO REPLAY-1861 caller thread of .map is unknown? - imageWireframeHelper.createImageWireframe( - view = view, - currentWireframeIndex = wireframes.size, - x = contentXPosInDp, - y = contentYPosInDp, - width = contentWidthPx, - height = contentHeightPx, - drawable = contentDrawable, - usePIIPlaceholder = true, - shapeStyle = null, - border = null, - clipping = clipping, - prefix = ImageWireframeHelper.DRAWABLE_CHILD_NAME, - imageWireframeHelperCallback = object : ImageWireframeHelperCallback { - override fun onFinished() { - asyncJobStatusCallback.jobFinished() - } - - override fun onStart() { - asyncJobStatusCallback.jobStarted() - } + if (contentDrawable != null) { + // resolve foreground + @Suppress("ThreadSafety") // TODO REPLAY-1861 caller thread of .map is unknown? + mappingContext.imageWireframeHelper.createImageWireframe( + view = view, + currentWireframeIndex = wireframes.size, + x = contentXPosInDp, + y = contentYPosInDp, + width = contentWidthPx, + height = contentHeightPx, + usePIIPlaceholder = true, + drawable = contentDrawable, + asyncJobStatusCallback = asyncJobStatusCallback, + clipping = clipping, + shapeStyle = null, + border = null, + prefix = ImageWireframeHelper.DRAWABLE_CHILD_NAME + )?.let { + wireframes.add(it) } - )?.let { - wireframes.add(it) } return wireframes diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckBoxMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckBoxMapper.kt index 7d0500f770..dd15176f52 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckBoxMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckBoxMapper.kt @@ -8,16 +8,24 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.CheckBox import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal class MaskCheckBoxMapper( textWireframeMapper: TextViewMapper, - stringUtils: StringUtils = StringUtils, - uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator, - viewUtils: ViewUtils = ViewUtils -) : CheckBoxMapper(textWireframeMapper, stringUtils, uniqueIdentifierGenerator, viewUtils) { + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : CheckBoxMapper( + textWireframeMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) { override fun resolveCheckedShapeStyle(view: CheckBox, checkBoxColor: String): MobileSegment.ShapeStyle? { // in case the MASK rule is applied we do not want to show the selection in the diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckedTextViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckedTextViewMapper.kt index 46984b5e0e..e8031487b8 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckedTextViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckedTextViewMapper.kt @@ -8,21 +8,23 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.CheckedTextView import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal class MaskCheckedTextViewMapper( textWireframeMapper: TextViewMapper, - stringUtils: StringUtils = StringUtils, - uniqueIdentifierGenerator: UniqueIdentifierGenerator = - UniqueIdentifierGenerator, - viewUtils: ViewUtils = ViewUtils + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper ) : CheckedTextViewMapper( textWireframeMapper, - stringUtils, - uniqueIdentifierGenerator, - viewUtils + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper ) { override fun resolveCheckedShapeStyle(view: CheckedTextView, checkBoxColor: String): MobileSegment.ShapeStyle? { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskInputTextViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskInputTextViewMapper.kt index 6b2d51d34a..cd9f0a6fe8 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskInputTextViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskInputTextViewMapper.kt @@ -9,22 +9,24 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.TextView import com.datadog.android.sessionreplay.SessionReplayPrivacy import com.datadog.android.sessionreplay.internal.recorder.obfuscator.rules.MaskInputObfuscationRule -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver /** * A [WireframeMapper] implementation to map a [TextView] component and apply the * [SessionReplayPrivacy.MASK_USER_INPUT] masking rule. */ -open class MaskInputTextViewMapper : TextViewMapper { - constructor() : super(textValueObfuscationRule = MaskInputObfuscationRule()) - - internal constructor( - imageWireframeHelper: ImageWireframeHelper, - uniqueIdentifierGenerator: UniqueIdentifierGenerator - ) : super( - imageWireframeHelper = imageWireframeHelper, - uniqueIdentifierGenerator = uniqueIdentifierGenerator, - textValueObfuscationRule = MaskInputObfuscationRule() - ) -} +open class MaskInputTextViewMapper( + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : TextViewMapper( + MaskInputObfuscationRule(), + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskNumberPickerMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskNumberPickerMapper.kt index 42743caa5f..fbb949d98c 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskNumberPickerMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskNumberPickerMapper.kt @@ -9,35 +9,37 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.os.Build import android.widget.NumberPicker import androidx.annotation.RequiresApi -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.SystemInformation import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver @RequiresApi(Build.VERSION_CODES.Q) internal open class MaskNumberPickerMapper( - stringUtils: StringUtils = StringUtils, - private val viewUtils: ViewUtils = ViewUtils, - private val uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator -) : BasePickerMapper(stringUtils, viewUtils) { + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : BasePickerMapper(viewIdentifierResolver, colorStringFormatter, viewBoundsResolver, drawableToColorMapper) { override fun map( view: NumberPicker, mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback ): List { - val selectedIndexLabelId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + val selectedIndexLabelId = viewIdentifierResolver.resolveChildUniqueIdentifier( view, SELECTED_INDEX_KEY_NAME ) - val topDividerId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + val topDividerId = viewIdentifierResolver.resolveChildUniqueIdentifier( view, DIVIDER_TOP_KEY_NAME ) - val bottomDividerId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + val bottomDividerId = viewIdentifierResolver.resolveChildUniqueIdentifier( view, DIVIDER_BOTTOM_KEY_NAME ) @@ -67,10 +69,7 @@ internal open class MaskNumberPickerMapper( bottomDividerId: Long ): List { val screenDensity = systemInformation.screenDensity - val viewGlobalBounds = viewUtils.resolveViewGlobalBounds( - view, - screenDensity - ) + val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds(view, screenDensity) val textSize = resolveTextSize(view, screenDensity) val labelHeight = textSize * 2 val paddingStart = resolveDividerPaddingStart(view, screenDensity) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskRadioButtonMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskRadioButtonMapper.kt index 7db1c07c77..a2a9bb67b1 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskRadioButtonMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskRadioButtonMapper.kt @@ -8,20 +8,23 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.RadioButton import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal class MaskRadioButtonMapper( textWireframeMapper: TextViewMapper, - stringUtils: StringUtils = StringUtils, - uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator, - viewUtils: ViewUtils = ViewUtils + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper ) : RadioButtonMapper( textWireframeMapper, - stringUtils, - uniqueIdentifierGenerator, - viewUtils + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper ) { override fun resolveCheckedShapeStyle(view: RadioButton, checkBoxColor: String): MobileSegment.ShapeStyle { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSeekBarWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSeekBarWireframeMapper.kt index 18de81da7b..75c7fe1dc2 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSeekBarWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSeekBarWireframeMapper.kt @@ -9,17 +9,23 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.os.Build import androidx.annotation.RequiresApi import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver @RequiresApi(Build.VERSION_CODES.O) internal class MaskSeekBarWireframeMapper( - viewUtils: ViewUtils = ViewUtils, - stringUtils: StringUtils = StringUtils, - uniqueIdentifierGenerator: UniqueIdentifierGenerator = - UniqueIdentifierGenerator -) : SeekBarWireframeMapper(viewUtils, stringUtils, uniqueIdentifierGenerator) { + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : SeekBarWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) { override fun resolveViewAsWireframesList( nonActiveTrackWireframe: MobileSegment.Wireframe.ShapeWireframe, diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSwitchCompatMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSwitchCompatMapper.kt index 98882fa95e..05a967f845 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSwitchCompatMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSwitchCompatMapper.kt @@ -9,16 +9,24 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import androidx.appcompat.widget.SwitchCompat import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal class MaskSwitchCompatMapper( textWireframeMapper: TextViewMapper, - stringUtils: StringUtils = StringUtils, - private val uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator, - viewUtils: ViewUtils = ViewUtils -) : SwitchCompatMapper(textWireframeMapper, stringUtils, uniqueIdentifierGenerator, viewUtils) { + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : SwitchCompatMapper( + textWireframeMapper, + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) { // region CheckableWireframeMapper @@ -33,7 +41,7 @@ internal class MaskSwitchCompatMapper( view: SwitchCompat, mappingContext: MappingContext ): List? { - val trackId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier(view, TRACK_KEY_NAME) + val trackId = viewIdentifierResolver.resolveChildUniqueIdentifier(view, TRACK_KEY_NAME) val trackThumbDimensions = resolveThumbAndTrackDimensions( view, mappingContext.systemInformation @@ -44,7 +52,7 @@ internal class MaskSwitchCompatMapper( val trackWidth = trackThumbDimensions[TRACK_WIDTH_INDEX] val trackHeight = trackThumbDimensions[TRACK_HEIGHT_INDEX] val checkableColor = resolveCheckableColor(view) - val viewGlobalBounds = resolveViewGlobalBounds( + val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds( view, mappingContext.systemInformation.screenDensity ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskTextViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskTextViewMapper.kt index 463dffa771..9361cb1bda 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskTextViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskTextViewMapper.kt @@ -7,33 +7,26 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.TextView -import androidx.annotation.VisibleForTesting import com.datadog.android.sessionreplay.SessionReplayPrivacy import com.datadog.android.sessionreplay.internal.recorder.obfuscator.rules.MaskObfuscationRule -import com.datadog.android.sessionreplay.internal.recorder.obfuscator.rules.TextValueObfuscationRule -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver /** * A [WireframeMapper] implementation to map a [TextView] component and apply the * [SessionReplayPrivacy.MASK] masking rule. */ -open class MaskTextViewMapper : TextViewMapper { - constructor() : super(textValueObfuscationRule = MaskObfuscationRule()) - - internal constructor( - imageWireframeHelper: ImageWireframeHelper, - uniqueIdentifierGenerator: UniqueIdentifierGenerator - ) : super( - imageWireframeHelper = imageWireframeHelper, - uniqueIdentifierGenerator = uniqueIdentifierGenerator, - textValueObfuscationRule = MaskObfuscationRule() - ) - - @VisibleForTesting - internal constructor( - imageWireframeHelper: ImageWireframeHelper, - uniqueIdentifierGenerator: UniqueIdentifierGenerator, - textValueObfuscationRule: TextValueObfuscationRule - ) : super(imageWireframeHelper, uniqueIdentifierGenerator, textValueObfuscationRule) -} +open class MaskTextViewMapper( + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : TextViewMapper( + MaskObfuscationRule(), + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/NumberPickerMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/NumberPickerMapper.kt index 3cd0ac845b..aad9c75037 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/NumberPickerMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/NumberPickerMapper.kt @@ -9,43 +9,45 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.os.Build import android.widget.NumberPicker import androidx.annotation.RequiresApi -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.SystemInformation import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver @RequiresApi(Build.VERSION_CODES.Q) internal open class NumberPickerMapper( - stringUtils: StringUtils = StringUtils, - private val viewUtils: ViewUtils = ViewUtils, - private val uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator -) : BasePickerMapper(stringUtils, viewUtils) { + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : BasePickerMapper(viewIdentifierResolver, colorStringFormatter, viewBoundsResolver, drawableToColorMapper) { override fun map( view: NumberPicker, mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback ): List { - val prevIndexLabelId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + val prevIndexLabelId = viewIdentifierResolver.resolveChildUniqueIdentifier( view, PREV_INDEX_KEY_NAME ) - val selectedIndexLabelId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + val selectedIndexLabelId = viewIdentifierResolver.resolveChildUniqueIdentifier( view, SELECTED_INDEX_KEY_NAME ) - val topDividerId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + val topDividerId = viewIdentifierResolver.resolveChildUniqueIdentifier( view, DIVIDER_TOP_KEY_NAME ) - val bottomDividerId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + val bottomDividerId = viewIdentifierResolver.resolveChildUniqueIdentifier( view, DIVIDER_BOTTOM_KEY_NAME ) - val nextIndexLabelId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + val nextIndexLabelId = viewIdentifierResolver.resolveChildUniqueIdentifier( view, NEXT_INDEX_KEY_NAME ) @@ -82,7 +84,7 @@ internal open class NumberPickerMapper( nextIndexLabelId: Long ): List { val screenDensity = systemInformation.screenDensity - val viewGlobalBounds = viewUtils.resolveViewGlobalBounds( + val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds( view, screenDensity ) @@ -91,7 +93,7 @@ internal open class NumberPickerMapper( val paddingStart = resolveDividerPaddingStart(view, screenDensity) val paddingEnd = resolveDividerPaddingEnd(view, screenDensity) val textColor = resolveSelectedTextColor(view) - val nextPrevLabelTextColor = colorAndAlphaAsStringHexa( + val nextPrevLabelTextColor = colorStringFormatter.formatColorAndAlphaAsHexString( view.textColor, PARTIALLY_OPAQUE_ALPHA_VALUE ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/QueueableViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/QueueableViewMapper.kt index 494a24ab95..fd9f8082cb 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/QueueableViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/QueueableViewMapper.kt @@ -7,10 +7,10 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.view.View -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback internal class QueueableViewMapper( private val mapper: WireframeMapper, diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/RadioButtonMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/RadioButtonMapper.kt index 9c2c4cdd10..d294dfe1e7 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/RadioButtonMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/RadioButtonMapper.kt @@ -8,20 +8,23 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.RadioButton import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal open class RadioButtonMapper( textWireframeMapper: TextViewMapper, - stringUtils: StringUtils = StringUtils, - uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator, - viewUtils: ViewUtils = ViewUtils + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper ) : CheckableCompoundButtonMapper( textWireframeMapper, - stringUtils, - uniqueIdentifierGenerator, - viewUtils + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper ) { // region CheckableTextViewMapper diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapper.kt index 728a16c63d..1288ae04e5 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapper.kt @@ -11,37 +11,43 @@ import android.content.res.Configuration import android.os.Build import android.widget.SeekBar import androidx.annotation.RequiresApi -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver @RequiresApi(Build.VERSION_CODES.O) internal open class SeekBarWireframeMapper( - private val viewUtils: ViewUtils = ViewUtils, - private val stringUtils: StringUtils = StringUtils, - private val uniqueIdentifierGenerator: UniqueIdentifierGenerator = - UniqueIdentifierGenerator -) : BaseWireframeMapper(stringUtils, viewUtils) { + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : BaseWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) { @Suppress("LongMethod") override fun map(view: SeekBar, mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback): List { - val activeTrackId = uniqueIdentifierGenerator + val activeTrackId = viewIdentifierResolver .resolveChildUniqueIdentifier(view, TRACK_ACTIVE_KEY_NAME) - val nonActiveTrackId = uniqueIdentifierGenerator + val nonActiveTrackId = viewIdentifierResolver .resolveChildUniqueIdentifier(view, TRACK_NON_ACTIVE_KEY_NAME) - val thumbId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier(view, THUMB_KEY_NAME) + val thumbId = viewIdentifierResolver.resolveChildUniqueIdentifier(view, THUMB_KEY_NAME) if (activeTrackId == null || thumbId == null || nonActiveTrackId == null) { return emptyList() } val screenDensity = mappingContext.systemInformation.screenDensity - val viewGlobalBounds = viewUtils.resolveViewGlobalBounds(view, screenDensity) + val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds(view, screenDensity) val normalizedSliderValue = view.normalizedValue() val viewAlpha = view.alpha @@ -52,15 +58,15 @@ internal open class SeekBarWireframeMapper( // colors val trackActiveColor = view.getTrackColor() val thumbColor = view.getThumbColor() - val trackActiveColorAsHexa = stringUtils.formatColorAndAlphaAsHexa( + val trackActiveColorAsHexa = colorStringFormatter.formatColorAndAlphaAsHexString( trackActiveColor, OPAQUE_ALPHA_VALUE ) - val trackNonActiveColorAsHexa = stringUtils.formatColorAndAlphaAsHexa( + val trackNonActiveColorAsHexa = colorStringFormatter.formatColorAndAlphaAsHexString( trackActiveColor, PARTIALLY_OPAQUE_ALPHA_VALUE ) - val thumbColorAsHexa = stringUtils.formatColorAndAlphaAsHexa(thumbColor, OPAQUE_ALPHA_VALUE) + val thumbColorAsHexa = colorStringFormatter.formatColorAndAlphaAsHexString(thumbColor, OPAQUE_ALPHA_VALUE) // track dimensions val trackBounds = view.progressDrawable.bounds diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt index e88bfcceaf..d396d1c95d 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt @@ -8,21 +8,28 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.graphics.Rect import androidx.appcompat.widget.SwitchCompat -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.SystemInformation import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver internal open class SwitchCompatMapper( private val textWireframeMapper: TextViewMapper, - private val stringUtils: StringUtils = StringUtils, - private val uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator, - viewUtils: ViewUtils = ViewUtils -) : CheckableWireframeMapper(viewUtils) { + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : CheckableWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) { // region CheckableWireframeMapper @@ -39,8 +46,8 @@ internal open class SwitchCompatMapper( view: SwitchCompat, mappingContext: MappingContext ): List? { - val thumbId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier(view, THUMB_KEY_NAME) - val trackId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier(view, TRACK_KEY_NAME) + val thumbId = viewIdentifierResolver.resolveChildUniqueIdentifier(view, THUMB_KEY_NAME) + val trackId = viewIdentifierResolver.resolveChildUniqueIdentifier(view, TRACK_KEY_NAME) val trackThumbDimensions = resolveThumbAndTrackDimensions( view, mappingContext.systemInformation @@ -53,7 +60,7 @@ internal open class SwitchCompatMapper( val thumbHeight = trackThumbDimensions[THUMB_HEIGHT_INDEX] val thumbWidth = trackThumbDimensions[THUMB_WIDTH_INDEX] val checkableColor = resolveCheckableColor(view) - val viewGlobalBounds = resolveViewGlobalBounds( + val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds( view, mappingContext.systemInformation.screenDensity ) @@ -84,8 +91,8 @@ internal open class SwitchCompatMapper( view: SwitchCompat, mappingContext: MappingContext ): List? { - val thumbId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier(view, THUMB_KEY_NAME) - val trackId = uniqueIdentifierGenerator.resolveChildUniqueIdentifier(view, TRACK_KEY_NAME) + val thumbId = viewIdentifierResolver.resolveChildUniqueIdentifier(view, THUMB_KEY_NAME) + val trackId = viewIdentifierResolver.resolveChildUniqueIdentifier(view, TRACK_KEY_NAME) val trackThumbDimensions = resolveThumbAndTrackDimensions( view, mappingContext.systemInformation @@ -98,7 +105,7 @@ internal open class SwitchCompatMapper( val thumbHeight = trackThumbDimensions[THUMB_HEIGHT_INDEX] val thumbWidth = trackThumbDimensions[THUMB_WIDTH_INDEX] val checkableColor = resolveCheckableColor(view) - val viewGlobalBounds = resolveViewGlobalBounds( + val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds( view, mappingContext.systemInformation.screenDensity ) @@ -130,7 +137,7 @@ internal open class SwitchCompatMapper( // region Internal protected fun resolveCheckableColor(view: SwitchCompat): String { - return stringUtils.formatColorAndAlphaAsHexa(view.currentTextColor, OPAQUE_ALPHA_VALUE) + return colorStringFormatter.formatColorAndAlphaAsHexString(view.currentTextColor, OPAQUE_ALPHA_VALUE) } private fun resolveThumbShapeStyle(view: SwitchCompat, checkBoxColor: String): MobileSegment.ShapeStyle { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/TextViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/TextViewMapper.kt index 0910c858a1..93e0c69041 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/TextViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/TextViewMapper.kt @@ -9,16 +9,17 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.graphics.Typeface import android.view.Gravity import android.widget.TextView -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.internal.recorder.obfuscator.rules.AllowObfuscationRule import com.datadog.android.sessionreplay.internal.recorder.obfuscator.rules.TextValueObfuscationRule -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelperCallback import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver /** * A [WireframeMapper] implementation to map a [TextView] component. @@ -27,32 +28,31 @@ import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator * All the other text fields will not be masked. */ @Suppress("TooManyFunctions") -open class TextViewMapper : - BaseAsyncBackgroundWireframeMapper { - - internal var textValueObfuscationRule: TextValueObfuscationRule = AllowObfuscationRule() - private var imageWireframeHelper: ImageWireframeHelper? = null - private var uniqueIdentifierGenerator: UniqueIdentifierGenerator? = null - - constructor() - - internal constructor( - imageWireframeHelper: ImageWireframeHelper, - uniqueIdentifierGenerator: UniqueIdentifierGenerator, - textValueObfuscationRule: TextValueObfuscationRule? = null - ) : super(imageWireframeHelper, uniqueIdentifierGenerator) { - this.imageWireframeHelper = imageWireframeHelper - this.uniqueIdentifierGenerator = uniqueIdentifierGenerator - textValueObfuscationRule?.let { - this.textValueObfuscationRule = it - } - } - - internal constructor( - textValueObfuscationRule: TextValueObfuscationRule - ) { - this.textValueObfuscationRule = textValueObfuscationRule - } +open class TextViewMapper internal constructor( + internal var textValueObfuscationRule: TextValueObfuscationRule, + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : BaseAsyncBackgroundWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) { + + constructor( + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper + ) : this( + AllowObfuscationRule(), + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper + ) override fun map( view: TextView, @@ -64,7 +64,7 @@ open class TextViewMapper : wireframes.addAll(super.map(view, mappingContext, asyncJobStatusCallback)) val density = mappingContext.systemInformation.screenDensity - val viewGlobalBounds = resolveViewGlobalBounds( + val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds( view, density ) @@ -88,30 +88,17 @@ open class TextViewMapper : // region Internal private fun resolveImages( - view: TextView, + textView: TextView, mappingContext: MappingContext, currentIndex: Int, asyncJobStatusCallback: AsyncJobStatusCallback ): List { - val wireframes = mutableListOf() - imageWireframeHelper?.let { - val results = it.createCompoundDrawableWireframes( - view, - mappingContext, - currentIndex, - object : ImageWireframeHelperCallback { - override fun onFinished() { - asyncJobStatusCallback.jobFinished() - } - - override fun onStart() { - asyncJobStatusCallback.jobStarted() - } - } - ) - wireframes.addAll(results) - } - return wireframes + return mappingContext.imageWireframeHelper.createCompoundDrawableWireframes( + textView, + mappingContext, + currentIndex, + asyncJobStatusCallback + ) } private fun resolveTextElements( @@ -150,16 +137,16 @@ open class TextViewMapper : return if (textView.text.isNullOrEmpty()) { resolveHintTextColor(textView) } else { - colorAndAlphaAsStringHexa(textView.currentTextColor, OPAQUE_ALPHA_VALUE) + colorStringFormatter.formatColorAndAlphaAsHexString(textView.currentTextColor, OPAQUE_ALPHA_VALUE) } } private fun resolveHintTextColor(textView: TextView): String { val hintTextColors = textView.hintTextColors return if (hintTextColors != null) { - colorAndAlphaAsStringHexa(hintTextColors.defaultColor, OPAQUE_ALPHA_VALUE) + colorStringFormatter.formatColorAndAlphaAsHexString(hintTextColors.defaultColor, OPAQUE_ALPHA_VALUE) } else { - colorAndAlphaAsStringHexa(textView.currentTextColor, OPAQUE_ALPHA_VALUE) + colorStringFormatter.formatColorAndAlphaAsHexString(textView.currentTextColor, OPAQUE_ALPHA_VALUE) } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/UnsupportedViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/UnsupportedViewMapper.kt index dac8ff7a86..6c3442303d 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/UnsupportedViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/UnsupportedViewMapper.kt @@ -7,21 +7,31 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.view.View -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.ViewUtilsInternal import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver -internal class UnsupportedViewMapper(viewUtils: ViewUtils = ViewUtils) : - BaseWireframeMapper(viewUtils = viewUtils) { +internal class UnsupportedViewMapper( + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : BaseWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) { override fun map(view: View, mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback): List { - val viewGlobalBounds = resolveViewGlobalBounds( - view, - mappingContext.systemInformation.screenDensity - ) + val pixelsDensity = mappingContext.systemInformation.screenDensity + val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds(view, pixelsDensity) return listOf( MobileSegment.Wireframe.PlaceholderWireframe( diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewScreenshotWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewScreenshotWireframeMapper.kt deleted file mode 100644 index d8cee19937..0000000000 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewScreenshotWireframeMapper.kt +++ /dev/null @@ -1,29 +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.sessionreplay.internal.recorder.mapper - -import android.view.View -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback -import com.datadog.android.sessionreplay.internal.recorder.MappingContext -import com.datadog.android.sessionreplay.model.MobileSegment - -// TODO: RUMM-0000 This should take a screenshot of the current view and return it as -// and ImageWireframe. It will be handled in the Session Replay v1. -internal class ViewScreenshotWireframeMapper( - private val viewWireframeMapper: ViewWireframeMapper = ViewWireframeMapper() -) : BaseWireframeMapper() { - - override fun map( - view: View, - mappingContext: MappingContext, - asyncJobStatusCallback: AsyncJobStatusCallback - ): List { - return viewWireframeMapper.map(view, mappingContext).map { - it.copy(border = MobileSegment.ShapeBorder("#000000ff", 1)) - } - } -} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewWireframeMapper.kt index d1d595223e..5f38570a53 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewWireframeMapper.kt @@ -7,24 +7,37 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.view.View -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver -internal class ViewWireframeMapper : - BaseWireframeMapper() { +internal class ViewWireframeMapper( + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper +) : BaseWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) { override fun map( view: View, mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback ): List { - val viewGlobalBounds = resolveViewGlobalBounds( + val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds( view, mappingContext.systemInformation.screenDensity ) - val (shapeStyle, border) = view.background?.resolveShapeStyleAndBorder(view.alpha) - ?: (null to null) + val shapeStyle = view.background?.let { resolveShapeStyle(it, view.alpha) } + return listOf( MobileSegment.Wireframe.ShapeWireframe( resolveViewId(view), @@ -33,7 +46,7 @@ internal class ViewWireframeMapper : viewGlobalBounds.width, viewGlobalBounds.height, shapeStyle = shapeStyle, - border = border + border = null ) ) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper.kt index 50b6725811..d93e638d8d 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper.kt @@ -7,11 +7,10 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.view.View -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback -import com.datadog.android.sessionreplay.internal.NoOpAsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.SystemInformation import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback /** * Maps a View to a [List] of [MobileSegment.Wireframe]. @@ -37,6 +36,6 @@ interface WireframeMapper { fun map( view: T, mappingContext: MappingContext, - asyncJobStatusCallback: AsyncJobStatusCallback = NoOpAsyncJobStatusCallback() + asyncJobStatusCallback: AsyncJobStatusCallback ): List } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/AndroidNStringObfuscator.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/AndroidNStringObfuscator.kt new file mode 100644 index 0000000000..5c4e92a450 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/AndroidNStringObfuscator.kt @@ -0,0 +1,37 @@ +/* + * 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.sessionreplay.internal.recorder.obfuscator + +import android.os.Build +import androidx.annotation.RequiresApi +import com.datadog.android.sessionreplay.internal.recorder.obfuscator.StringObfuscator.Companion.CHARACTER_MASK + +/** + * String obfuscator relying on Android N APIs to properly handle strings with emojis. + */ +@RequiresApi(Build.VERSION_CODES.N) +class AndroidNStringObfuscator : StringObfuscator { + override fun obfuscate(stringValue: String): String { + return buildString { + stringValue.codePoints().forEach { + if (Character.isWhitespace(it)) { + // I don't think we should log this case here. I could not even reproduce it + // in my tests. As long as there is a valid sdtring there there should not be + // any problem. + @Suppress("SwallowedException") + try { + append(Character.toChars(it)) + } catch (e: IllegalArgumentException) { + append(CHARACTER_MASK) + } + } else { + append(CHARACTER_MASK) + } + } + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/DefaultStringObfuscator.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/DefaultStringObfuscator.kt deleted file mode 100644 index f47abb385a..0000000000 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/DefaultStringObfuscator.kt +++ /dev/null @@ -1,65 +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.sessionreplay.internal.recorder.obfuscator - -import android.os.Build -import androidx.annotation.RequiresApi - -internal class DefaultStringObfuscator : StringObfuscator { - - override fun obfuscate(stringValue: String): String { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - obfuscateUsingCodeStream(stringValue) - } else { - obfuscateUsingCharacterCode(stringValue) - } - } - - private fun obfuscateUsingCharacterCode(stringValue: String): String { - return String( - CharArray(stringValue.length) { - val character = stringValue[it] - // Given that we replace each printable character with `x` for 2 chars expressions - // as emojis we will have 2 `x` instead of 1 but this will not be a problem as the - // obfuscation will still be applied. - if (Character.isWhitespace(character.code)) { - character - } else { - CHARACTER_MASK - } - } - ) - } - - @RequiresApi(Build.VERSION_CODES.N) - private fun obfuscateUsingCodeStream(stringValue: String): String { - // Because we are using the CharSequence.codePoints() stream we are going to correctly - // handle the cases where the text contains 2 chars expression. In this case one single - // codePoint will be returned for those 2 chars and the obfuscation char will be `x`. - val stringBuilder = StringBuilder() - stringValue.codePoints().forEach { - if (Character.isWhitespace(it)) { - // I don't think we should log this case here. I could not even reproduce it - // in my tests. As long as there is a valid string there there should not be - // any problem. - @Suppress("SwallowedException") - try { - stringBuilder.append(Character.toChars(it)) - } catch (e: IllegalArgumentException) { - stringBuilder.append(CHARACTER_MASK) - } - } else { - stringBuilder.append(CHARACTER_MASK) - } - } - return stringBuilder.toString() - } - - companion object { - private const val CHARACTER_MASK = 'x' - } -} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/LegacyStringObfuscator.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/LegacyStringObfuscator.kt new file mode 100644 index 0000000000..7f85ee0717 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/LegacyStringObfuscator.kt @@ -0,0 +1,28 @@ +/* + * 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.sessionreplay.internal.recorder.obfuscator + +import com.datadog.android.sessionreplay.internal.recorder.obfuscator.StringObfuscator.Companion.CHARACTER_MASK + +internal class LegacyStringObfuscator : StringObfuscator { + + override fun obfuscate(stringValue: String): String { + return String( + CharArray(stringValue.length) { + val character = stringValue[it] + // Given that we replace each printable character with `x` for 2 chars expressions + // as emojis we will have 2 `x` instead of 1 but this will not be a problem as the + // obfuscation will still be applied. + if (Character.isWhitespace(character.code)) { + character + } else { + CHARACTER_MASK + } + } + ) + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/StringObfuscator.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/StringObfuscator.kt index d7c5f5c27b..382bba66ef 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/StringObfuscator.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/StringObfuscator.kt @@ -6,6 +6,20 @@ package com.datadog.android.sessionreplay.internal.recorder.obfuscator +import android.os.Build + internal interface StringObfuscator { fun obfuscate(stringValue: String): String + + companion object { + internal const val CHARACTER_MASK = 'x' + + fun getStringObfuscator(): StringObfuscator { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + AndroidNStringObfuscator() + } else { + LegacyStringObfuscator() + } + } + } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/rules/MaskObfuscationRule.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/rules/MaskObfuscationRule.kt index 7c068c23c0..2c4dc1aaeb 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/rules/MaskObfuscationRule.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/rules/MaskObfuscationRule.kt @@ -8,18 +8,16 @@ package com.datadog.android.sessionreplay.internal.recorder.obfuscator.rules import android.widget.TextView import com.datadog.android.sessionreplay.internal.recorder.MappingContext -import com.datadog.android.sessionreplay.internal.recorder.obfuscator.DefaultStringObfuscator import com.datadog.android.sessionreplay.internal.recorder.obfuscator.FixedLengthStringObfuscator +import com.datadog.android.sessionreplay.internal.recorder.obfuscator.StringObfuscator internal class MaskObfuscationRule( - private val defaultStringObfuscator: DefaultStringObfuscator = - DefaultStringObfuscator(), - private val fixedLengthStringObfuscator: FixedLengthStringObfuscator = - FixedLengthStringObfuscator(), - private val textTypeResolver: TextTypeResolver = - TextTypeResolver(), + private val defaultStringObfuscator: StringObfuscator = StringObfuscator.getStringObfuscator(), + private val fixedLengthStringObfuscator: FixedLengthStringObfuscator = FixedLengthStringObfuscator(), + private val textTypeResolver: TextTypeResolver = TextTypeResolver(), private val textValueResolver: TextValueResolver = TextValueResolver() ) : TextValueObfuscationRule { + override fun resolveObfuscatedValue( textView: TextView, mappingContext: MappingContext diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManager.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManager.kt new file mode 100644 index 0000000000..2efede60d2 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManager.kt @@ -0,0 +1,74 @@ +/* + * 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.sessionreplay.internal.recorder.resources + +import android.content.ComponentCallbacks2 +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import androidx.annotation.MainThread +import com.datadog.android.api.InternalLogger + +internal class BitmapCachesManager( + private val resourcesLRUCache: Cache, + private val bitmapPool: BitmapPool, + private val logger: InternalLogger +) { + private var isResourcesCacheRegisteredForCallbacks: Boolean = false + private var isBitmapPoolRegisteredForCallbacks: Boolean = false + + @MainThread + internal fun registerCallbacks(applicationContext: Context) { + registerResourceLruCacheForCallbacks(applicationContext) + registerBitmapPoolForCallbacks(applicationContext) + } + + @MainThread + private fun registerResourceLruCacheForCallbacks(applicationContext: Context) { + if (isResourcesCacheRegisteredForCallbacks) return + + if (resourcesLRUCache is ComponentCallbacks2) { + applicationContext.registerComponentCallbacks(resourcesLRUCache) + isResourcesCacheRegisteredForCallbacks = true + } else { + logger.log( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + messageBuilder = { Cache.DOES_NOT_IMPLEMENT_COMPONENTCALLBACKS } + ) + } + } + + @MainThread + private fun registerBitmapPoolForCallbacks(applicationContext: Context) { + if (isBitmapPoolRegisteredForCallbacks) return + + applicationContext.registerComponentCallbacks(bitmapPool) + isBitmapPoolRegisteredForCallbacks = true + } + + internal fun putInResourceCache(drawable: Drawable, resourceId: String) { + resourcesLRUCache.put(drawable, resourceId.toByteArray(Charsets.UTF_8)) + } + + internal fun getFromResourceCache(drawable: Drawable): String? { + val resourceId = resourcesLRUCache.get(drawable) ?: return null + return String(resourceId, Charsets.UTF_8) + } + + internal fun putInBitmapPool(bitmap: Bitmap) { + bitmapPool.put(bitmap) + } + + internal fun getBitmapByProperties( + width: Int, + height: Int, + config: Bitmap.Config + ): Bitmap? { + return bitmapPool.getBitmapByProperties(width, height, config) + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageWireframeHelper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelper.kt similarity index 74% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageWireframeHelper.kt rename to features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelper.kt index 424bb69179..8e9ff852c3 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageWireframeHelper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelper.kt @@ -19,26 +19,25 @@ import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.ViewUtilsInternal import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ImageWireframeHelper +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import java.util.Locale -// This should not have a callback but it should just create a placeholder for resourcesSerializer -// The resourcesSerializer dependency should be removed from here -// TODO: RUM-0000 Remove the resourcesSerializer dependency from here -internal class ImageWireframeHelper( +// This should not have a callback but it should just create a placeholder for resourceResolver +// The resourceResolver dependency should be removed from here +// TODO: RUM-0000 Remove the resourceResolver dependency from here +internal class DefaultImageWireframeHelper( private val logger: InternalLogger, - private val resourcesSerializer: ResourcesSerializer, - private val imageCompression: ImageCompression = WebPImageCompression(), - private val uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator, - private val viewUtilsInternal: ViewUtilsInternal = ViewUtilsInternal(), - private val imageTypeResolver: ImageTypeResolver = ImageTypeResolver() -) { - - // Why is this function accepting an optional drawable ??? - // TODO: RUM-0000 Make the drawable non optional for this function + private val resourceResolver: ResourceResolver, + private val viewIdentifierResolver: ViewIdentifierResolver, + private val viewUtilsInternal: ViewUtilsInternal, + private val imageTypeResolver: ImageTypeResolver +) : ImageWireframeHelper { + @Suppress("ReturnCount") @MainThread - internal fun createImageWireframe( + override fun createImageWireframe( view: View, currentWireframeIndex: Int, x: Long, @@ -46,15 +45,14 @@ internal class ImageWireframeHelper( width: Int, height: Int, usePIIPlaceholder: Boolean, - clipping: MobileSegment.WireframeClip? = null, - drawable: Drawable? = null, - shapeStyle: MobileSegment.ShapeStyle? = null, - border: MobileSegment.ShapeBorder? = null, - imageWireframeHelperCallback: ImageWireframeHelperCallback, - prefix: String? = DRAWABLE_CHILD_NAME + drawable: Drawable, + asyncJobStatusCallback: AsyncJobStatusCallback, + clipping: MobileSegment.WireframeClip?, + shapeStyle: MobileSegment.ShapeStyle?, + border: MobileSegment.ShapeBorder?, + prefix: String? ): MobileSegment.Wireframe? { - if (drawable == null) return null - val id = uniqueIdentifierGenerator.resolveChildUniqueIdentifier(view, prefix + currentWireframeIndex) + val id = viewIdentifierResolver.resolveChildUniqueIdentifier(view, prefix + currentWireframeIndex) val drawableProperties = resolveDrawableProperties(view, drawable) if (id == null || !drawableProperties.isValid()) return null @@ -84,7 +82,6 @@ internal class ImageWireframeHelper( return null } - val mimeType = imageCompression.getMimeType() val density = displayMetrics.density // in case we suspect the image is PII, return a placeholder @@ -105,23 +102,26 @@ internal class ImageWireframeHelper( shapeStyle = shapeStyle, border = border, clip = clipping, - mimeType = mimeType, isEmpty = true ) - imageWireframeHelperCallback.onStart() + asyncJobStatusCallback.jobStarted() - resourcesSerializer.handleBitmap( + resourceResolver.resolveResourceId( resources = resources, applicationContext = applicationContext, displayMetrics = displayMetrics, drawable = drawableProperties.drawable, drawableWidth = width, drawableHeight = height, - imageWireframe = imageWireframe, - resourcesSerializerCallback = object : ResourcesSerializerCallback { - override fun onReady() { - imageWireframeHelperCallback.onFinished() + resourceResolverCallback = object : ResourceResolverCallback { + override fun onSuccess(resourceId: String) { + populateResourceIdInWireframe(resourceId, imageWireframe) + asyncJobStatusCallback.jobFinished() + } + + override fun onFailure() { + asyncJobStatusCallback.jobFinished() } } ) @@ -130,11 +130,11 @@ internal class ImageWireframeHelper( } @Suppress("NestedBlockDepth") - internal fun createCompoundDrawableWireframes( - view: TextView, + override fun createCompoundDrawableWireframes( + textView: TextView, mappingContext: MappingContext, prevWireframeIndex: Int, - imageWireframeHelperCallback: ImageWireframeHelperCallback + asyncJobStatusCallback: AsyncJobStatusCallback ): MutableList { val result = mutableListOf() var wireframeIndex = prevWireframeIndex @@ -142,7 +142,7 @@ internal class ImageWireframeHelper( // CompoundDrawables returns an array of indexes in the following order: // left, top, right, bottom - view.compoundDrawables.forEachIndexed { compoundDrawableIndex, _ -> + textView.compoundDrawables.forEachIndexed { compoundDrawableIndex, _ -> if (compoundDrawableIndex > CompoundDrawablePositions.values().size) { return@forEachIndexed } @@ -151,18 +151,18 @@ internal class ImageWireframeHelper( compoundDrawableIndex ) ?: return@forEachIndexed - val drawable = view.compoundDrawables[compoundDrawableIndex] + val drawable = textView.compoundDrawables[compoundDrawableIndex] if (drawable != null) { val drawableCoordinates = viewUtilsInternal.resolveCompoundDrawableBounds( - view = view, + view = textView, drawable = drawable, pixelsDensity = density, position = compoundDrawablePosition ) @Suppress("ThreadSafety") // TODO REPLAY-1861 caller thread of .map is unknown? createImageWireframe( - view = view, + view = textView, currentWireframeIndex = ++wireframeIndex, x = drawableCoordinates.x, y = drawableCoordinates.y, @@ -173,7 +173,7 @@ internal class ImageWireframeHelper( border = null, usePIIPlaceholder = true, clipping = MobileSegment.WireframeClip(), - imageWireframeHelperCallback = imageWireframeHelperCallback + asyncJobStatusCallback = asyncJobStatusCallback )?.let { resultWireframe -> result.add(resultWireframe) } @@ -193,6 +193,7 @@ internal class ImageWireframeHelper( DrawableProperties(drawable, drawable.intrinsicWidth, drawable.intrinsicHeight) } } + is InsetDrawable -> { val internalDrawable = drawable.drawable if (internalDrawable != null) { @@ -201,6 +202,7 @@ internal class ImageWireframeHelper( DrawableProperties(drawable, drawable.intrinsicWidth, drawable.intrinsicHeight) } } + is GradientDrawable -> DrawableProperties(drawable, view.width, view.height) else -> DrawableProperties(drawable, drawable.intrinsicWidth, drawable.intrinsicHeight) } @@ -212,8 +214,7 @@ internal class ImageWireframeHelper( density: Float ): MobileSegment.Wireframe.PlaceholderWireframe { val coordinates = IntArray(2) - // this will always have size >= 2 - @Suppress("UnsafeThirdPartyFunctionCall") + @Suppress("UnsafeThirdPartyFunctionCall") // this will always have size >= 2 view.getLocationOnScreen(coordinates) val viewX = coordinates[0].densityNormalized(density).toLong() val viewY = coordinates[1].densityNormalized(density).toLong() @@ -256,15 +257,23 @@ internal class ImageWireframeHelper( } } + private fun populateResourceIdInWireframe( + resourceId: String, + wireframe: MobileSegment.Wireframe.ImageWireframe + ) { + wireframe.resourceId = resourceId + wireframe.isEmpty = false + } + internal companion object { - internal const val DRAWABLE_CHILD_NAME = "drawable" - @VisibleForTesting internal const val PLACEHOLDER_CONTENT_LABEL = "Content Image" + @VisibleForTesting + internal const val PLACEHOLDER_CONTENT_LABEL = "Content Image" - @VisibleForTesting internal const val APPLICATION_CONTEXT_NULL_ERROR = - "Application context is null for view %s" + @VisibleForTesting + internal const val APPLICATION_CONTEXT_NULL_ERROR = "Application context is null for view %s" - @VisibleForTesting internal const val RESOURCES_NULL_ERROR = - "Resources is null for view %s" + @VisibleForTesting + internal const val RESOURCES_NULL_ERROR = "Resources is null for view %s" } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageCompression.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageCompression.kt index 16155c64e4..5d168c2409 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageCompression.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageCompression.kt @@ -14,11 +14,6 @@ import java.io.ByteArrayOutputStream */ internal interface ImageCompression { - /** - * Get the mimetype for the image format. - */ - fun getMimeType(): String? - /** * Compress the bitmap to a [ByteArrayOutputStream]. */ diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/MD5HashGenerator.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/MD5HashGenerator.kt index fb9bdde5e3..c8b0a0bbce 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/MD5HashGenerator.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/MD5HashGenerator.kt @@ -25,7 +25,7 @@ internal class MD5HashGenerator( } catch (e: NoSuchAlgorithmException) { logger.log( InternalLogger.Level.ERROR, - InternalLogger.Target.USER, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), { MD5_HASH_GENERATION_ERROR }, e ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageWireframeHelperCallback.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResolveResourceCallback.kt similarity index 60% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageWireframeHelperCallback.kt rename to features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResolveResourceCallback.kt index a1aacc3535..87a0e89fe4 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageWireframeHelperCallback.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResolveResourceCallback.kt @@ -6,9 +6,7 @@ package com.datadog.android.sessionreplay.internal.recorder.resources -// TODO: RUM-0000 this should be removed once we switch to a synchronous implementation of -// the ImageWireframeHelper -internal interface ImageWireframeHelperCallback { - fun onStart() - fun onFinished() +internal interface ResolveResourceCallback { + fun onResolved(resourceId: String, resourceData: ByteArray) + fun onFailed() } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceItemCreationHandler.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceItemCreationHandler.kt new file mode 100644 index 0000000000..af5e4c778d --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceItemCreationHandler.kt @@ -0,0 +1,34 @@ +/* + * 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.sessionreplay.internal.recorder.resources + +import androidx.annotation.VisibleForTesting +import com.datadog.android.sessionreplay.internal.async.DataQueueHandler +import java.util.Collections + +internal class ResourceItemCreationHandler( + private val recordedDataQueueHandler: DataQueueHandler, + private val applicationId: String +) { + // resource IDs previously sent in this session - + // optimization to avoid sending the same resource multiple times + // atm this set is unbounded but expected to use relatively little space (~80kb per 1k items) + @VisibleForTesting internal val resourceIdsSeen: MutableSet = + Collections.synchronizedSet(HashSet()) + + internal fun queueItem(resourceId: String, resourceData: ByteArray) { + if (!resourceIdsSeen.contains(resourceId)) { + resourceIdsSeen.add(resourceId) + + recordedDataQueueHandler.addResourceItem( + identifier = resourceId, + resourceData = resourceData, + applicationId = applicationId + ) + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolver.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolver.kt new file mode 100644 index 0000000000..0135cc13b6 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolver.kt @@ -0,0 +1,295 @@ +/* + * 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.sessionreplay.internal.recorder.resources + +import android.content.Context +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.DisplayMetrics +import androidx.annotation.MainThread +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.utils.executeSafe +import com.datadog.android.sessionreplay.internal.async.DataQueueHandler +import com.datadog.android.sessionreplay.internal.utils.DrawableUtils +import java.util.concurrent.ExecutorService +import java.util.concurrent.LinkedBlockingDeque +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit + +@Suppress("TooManyFunctions") +internal class ResourceResolver( + private val bitmapCachesManager: BitmapCachesManager, + internal val threadPoolExecutor: ExecutorService = THREADPOOL_EXECUTOR, + private val drawableUtils: DrawableUtils, + private val webPImageCompression: ImageCompression, + private val logger: InternalLogger, + private val md5HashGenerator: MD5HashGenerator, + private val recordedDataQueueHandler: DataQueueHandler, + private val applicationId: String, + private val resourceItemCreationHandler: ResourceItemCreationHandler = ResourceItemCreationHandler( + recordedDataQueueHandler = recordedDataQueueHandler, + applicationId = applicationId + ) +) { + + // region internal + @MainThread + internal fun resolveResourceId( + resources: Resources, + applicationContext: Context, + displayMetrics: DisplayMetrics, + drawable: Drawable, + drawableWidth: Int, + drawableHeight: Int, + resourceResolverCallback: ResourceResolverCallback + ) { + bitmapCachesManager.registerCallbacks(applicationContext) + + val resourceId = tryToGetResourceFromCache(drawable = drawable) + + if (resourceId != null) { + // if we got here it means we saw the bitmap before, + // so we don't need to send the resource again + resourceResolverCallback.onSuccess(resourceId) + return + } + + val bitmapFromDrawable = + if (drawable is BitmapDrawable && shouldUseDrawableBitmap(drawable)) { + drawable.bitmap // cannot be null - we already checked in shouldUseDrawableBitmap + } else { + null + } + + // do in the background + threadPoolExecutor.executeSafe("resolveResourceId", logger) { + @Suppress("ThreadSafety") // this runs inside an executor + createBitmap( + resources = resources, + drawable = drawable, + drawableWidth = drawableWidth, + drawableHeight = drawableHeight, + displayMetrics = displayMetrics, + bitmapFromDrawable = bitmapFromDrawable, + resolveResourceCallback = object : ResolveResourceCallback { + override fun onResolved(resourceId: String, resourceData: ByteArray) { + resourceItemCreationHandler.queueItem(resourceId, resourceData) + resourceResolverCallback.onSuccess(resourceId) + } + + override fun onFailed() { + resourceResolverCallback.onFailure() + } + } + ) + } + } + + // endregion + + // region private + + @WorkerThread + private fun createBitmap( + resources: Resources, + drawable: Drawable, + drawableWidth: Int, + drawableHeight: Int, + displayMetrics: DisplayMetrics, + bitmapFromDrawable: Bitmap?, + resolveResourceCallback: ResolveResourceCallback + ) { + val handledBitmap = if (bitmapFromDrawable != null) { + tryToGetBitmapFromBitmapDrawable( + drawable = drawable as BitmapDrawable, + bitmapFromDrawable = bitmapFromDrawable, + resolveResourceCallback = resolveResourceCallback + ) + } else { + null + } + + if (handledBitmap == null) { + tryToDrawNewBitmap( + resources = resources, + drawable = drawable, + drawableWidth = drawableWidth, + drawableHeight = drawableHeight, + displayMetrics = displayMetrics, + resolveResourceCallback = resolveResourceCallback + ) + } + } + + @Suppress("ReturnCount") + @WorkerThread + private fun resolveResourceHash( + drawable: Drawable, + bitmap: Bitmap, + compressedBitmapBytes: ByteArray, + shouldCacheBitmap: Boolean, + resolveResourceCallback: ResolveResourceCallback + ) { + // failed to get image data + if (compressedBitmapBytes.isEmpty()) { + // we are already logging the failure in webpImageCompression + resolveResourceCallback.onFailed() + return + } + + val resourceId = md5HashGenerator.generate(compressedBitmapBytes) + + // failed to resolve bitmap identifier + if (resourceId == null) { + // logging md5 generation failures inside md5HashGenerator + resolveResourceCallback.onFailed() + return + } + + cacheIfNecessary( + shouldCacheBitmap = shouldCacheBitmap, + bitmap = bitmap, + resourceId = resourceId, + drawable = drawable + ) + + resolveResourceCallback.onResolved(resourceId, compressedBitmapBytes) + } + + private fun cacheIfNecessary( + shouldCacheBitmap: Boolean, + bitmap: Bitmap, + resourceId: String, + drawable: Drawable + ) { + if (shouldCacheBitmap) { + bitmapCachesManager.putInBitmapPool(bitmap) + } + + bitmapCachesManager.putInResourceCache(drawable, resourceId) + } + + @WorkerThread + private fun tryToDrawNewBitmap( + resources: Resources, + drawable: Drawable, + drawableWidth: Int, + drawableHeight: Int, + displayMetrics: DisplayMetrics, + resolveResourceCallback: ResolveResourceCallback + ) { + drawableUtils.createBitmapOfApproxSizeFromDrawable( + resources = resources, + drawable = drawable, + drawableWidth = drawableWidth, + drawableHeight = drawableHeight, + displayMetrics = displayMetrics, + bitmapCreationCallback = object : BitmapCreationCallback { + override fun onReady(bitmap: Bitmap) { + val compressedBitmapBytes = webPImageCompression.compressBitmap(bitmap) + + // failed to compress bitmap + if (compressedBitmapBytes.isEmpty()) { + resolveResourceCallback.onFailed() + return + } + + @Suppress("ThreadSafety") // this runs inside an executor + resolveResourceHash( + drawable = drawable, + bitmap = bitmap, + compressedBitmapBytes = compressedBitmapBytes, + shouldCacheBitmap = true, + resolveResourceCallback = resolveResourceCallback + ) + } + + override fun onFailure() { + resolveResourceCallback.onFailed() + } + } + ) + } + + @WorkerThread + @Suppress("ReturnCount") + private fun tryToGetBitmapFromBitmapDrawable( + drawable: BitmapDrawable, + bitmapFromDrawable: Bitmap, + resolveResourceCallback: ResolveResourceCallback + ): Bitmap? { + val scaledBitmap = drawableUtils.createScaledBitmap(bitmapFromDrawable) + ?: return null + + val compressedBitmapBytes = webPImageCompression.compressBitmap(scaledBitmap) + + // failed to get byteArray potentially because the bitmap was recycled before imageCompression + if (compressedBitmapBytes.isEmpty()) { + return null + } + + /** + * Check whether the scaled bitmap is the same as the original. + * Since Bitmap.createScaledBitmap will return the original bitmap if the + * requested dimensions match the dimensions of the original + * Add a specific check for isRecycled, because getting width/height from a recycled bitmap + * is undefined behavior + */ + val shouldCacheBitmap = !bitmapFromDrawable.isRecycled && ( + scaledBitmap.width < bitmapFromDrawable.width || + scaledBitmap.height < bitmapFromDrawable.height + ) + + resolveResourceHash( + drawable = drawable, + bitmap = scaledBitmap, + compressedBitmapBytes = compressedBitmapBytes, + shouldCacheBitmap = shouldCacheBitmap, + resolveResourceCallback = resolveResourceCallback + ) + + return scaledBitmap + } + + private fun tryToGetResourceFromCache( + drawable: Drawable + ): String? = bitmapCachesManager.getFromResourceCache(drawable) + + private fun shouldUseDrawableBitmap(drawable: BitmapDrawable): Boolean { + return drawable.bitmap != null && + !drawable.bitmap.isRecycled && + drawable.bitmap.width > 0 && + drawable.bitmap.height > 0 + } + + // endregion + + internal interface BitmapCreationCallback { + fun onReady(bitmap: Bitmap) + fun onFailure() + } + + // endregion + + private companion object { + private const val THREAD_POOL_MAX_KEEP_ALIVE_MS = 5000L + private const val CORE_DEFAULT_POOL_SIZE = 1 + private const val MAX_THREAD_COUNT = 10 + + @Suppress("UnsafeThirdPartyFunctionCall") // all parameters are non-negative and queue is not null + private val THREADPOOL_EXECUTOR = ThreadPoolExecutor( + CORE_DEFAULT_POOL_SIZE, + MAX_THREAD_COUNT, + THREAD_POOL_MAX_KEEP_ALIVE_MS, + TimeUnit.MILLISECONDS, + LinkedBlockingDeque() + ) + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesSerializerCallback.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverCallback.kt similarity index 75% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesSerializerCallback.kt rename to features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverCallback.kt index a6bfce14fd..731cc05780 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesSerializerCallback.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverCallback.kt @@ -6,6 +6,8 @@ package com.datadog.android.sessionreplay.internal.recorder.resources -internal interface ResourcesSerializerCallback { - fun onReady() +internal interface ResourceResolverCallback { + fun onSuccess(resourceId: String) + + fun onFailure() } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesSerializer.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesSerializer.kt deleted file mode 100644 index 457d7ed56c..0000000000 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesSerializer.kt +++ /dev/null @@ -1,378 +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.sessionreplay.internal.recorder.resources - -import android.content.ComponentCallbacks2 -import android.content.Context -import android.content.res.Resources -import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable -import android.util.DisplayMetrics -import androidx.annotation.MainThread -import androidx.annotation.VisibleForTesting -import androidx.annotation.WorkerThread -import com.datadog.android.api.InternalLogger -import com.datadog.android.core.internal.utils.executeSafe -import com.datadog.android.sessionreplay.internal.async.DataQueueHandler -import com.datadog.android.sessionreplay.internal.async.NoopDataQueueHandler -import com.datadog.android.sessionreplay.internal.recorder.resources.Cache.Companion.DOES_NOT_IMPLEMENT_COMPONENTCALLBACKS -import com.datadog.android.sessionreplay.internal.utils.DrawableUtils -import com.datadog.android.sessionreplay.model.MobileSegment -import java.util.Collections -import java.util.concurrent.ExecutorService -import java.util.concurrent.LinkedBlockingDeque -import java.util.concurrent.ThreadPoolExecutor -import java.util.concurrent.TimeUnit - -@Suppress("TooManyFunctions") -internal class ResourcesSerializer private constructor( - private val threadPoolExecutor: ExecutorService, - private val drawableUtils: DrawableUtils, - private val webPImageCompression: ImageCompression, - private val resourcesLRUCache: Cache, - private val bitmapPool: BitmapPool?, - private val logger: InternalLogger, - private val md5HashGenerator: MD5HashGenerator, - private val recordedDataQueueHandler: DataQueueHandler, - private val applicationId: String -) { - private var isResourcesCacheRegisteredForCallbacks: Boolean = false - private var isBitmapPoolRegisteredForCallbacks: Boolean = false - - // resource IDs previously sent in this session - - // optimization to avoid sending the same resource multiple times - // atm this set is unbounded but expected to use relatively little space (~80kb per 1k items) - private val resourceIdsSeen: MutableSet = - Collections.synchronizedSet(HashSet()) - - // region internal - - @MainThread - internal fun handleBitmap( - resources: Resources, - applicationContext: Context, - displayMetrics: DisplayMetrics, - drawable: Drawable, - drawableWidth: Int, - drawableHeight: Int, - imageWireframe: MobileSegment.Wireframe.ImageWireframe, - resourcesSerializerCallback: ResourcesSerializerCallback - ) { - registerCallbacks(applicationContext) - - tryToGetResourceFromCache( - drawable = drawable, - imageWireframe = imageWireframe, - resourcesSerializerCallback = resourcesSerializerCallback - ) - ?: tryToGetBitmapFromBitmapDrawable( - resources = resources, - drawable = drawable, - displayMetrics = displayMetrics, - imageWireframe = imageWireframe, - resourcesSerializerCallback = resourcesSerializerCallback - ) - ?: tryToDrawNewBitmap( - resources = resources, - drawable = drawable, - drawableWidth = drawableWidth, - drawableHeight = drawableHeight, - displayMetrics = displayMetrics, - imageWireframe = imageWireframe, - resourcesSerializerCallback = resourcesSerializerCallback, - - // this parameter is used to avoid infinite recursion - // basically we only allow one attempt to recreate the bitmap - didCallOriginateFromFailover = false - ) - } - - // endregion - - // region testing - - @VisibleForTesting - internal fun getThreadPoolExecutor(): ExecutorService = threadPoolExecutor - - // endregion - - // region private - - @Suppress("ReturnCount") - @WorkerThread - private fun serializeBitmap( - resources: Resources, - drawable: Drawable, - displayMetrics: DisplayMetrics, - bitmap: Bitmap, - shouldCacheBitmap: Boolean, - imageWireframe: MobileSegment.Wireframe.ImageWireframe, - resourcesSerializerCallback: ResourcesSerializerCallback, - - // this parameter is used to avoid infinite recursion - // basically we only allow one attempt to recreate the bitmap - didCallOriginateFromFailover: Boolean - ) { - val byteArray = webPImageCompression.compressBitmap(bitmap) - - // failed to get byteArray potentially because the bitmap was recycled before imageCompression - // Try once to recreate bitmap from the drawable - if (byteArray.isEmpty() && bitmap.isRecycled && !didCallOriginateFromFailover) { - tryToDrawNewBitmap( - resources = resources, - drawable = drawable, - drawableWidth = bitmap.width, - drawableHeight = bitmap.height, - displayMetrics = displayMetrics, - imageWireframe = imageWireframe, - resourcesSerializerCallback = resourcesSerializerCallback, - didCallOriginateFromFailover = true - ) - - return - } - - if (byteArray.isEmpty()) { - // failed to get image data - resourcesSerializerCallback.onReady() - return - } - - val resourceId = md5HashGenerator.generate(byteArray) - - if (shouldCacheBitmap) { - bitmapPool?.put(bitmap) - } - - if (resourceId == null) { - // resourceId is mandatory for resource endpoint - resourcesSerializerCallback.onReady() - return - } - - if (!resourceIdsSeen.contains(resourceId)) { - resourceIdsSeen.add(resourceId) - - // We probably don't want this here. In the next pr we'll - // refactor this class and extract logic - recordedDataQueueHandler.addResourceItem( - identifier = resourceId, - resourceData = byteArray, - applicationId = applicationId - ) - } - - val resourceIdByteArray = resourceId.toByteArray(Charsets.UTF_8) - resourcesLRUCache.put(drawable, resourceIdByteArray) - - finalizeRecordedDataItem(resourceIdByteArray, imageWireframe) - resourcesSerializerCallback.onReady() - } - - @MainThread - private fun registerResourceLruCacheForCallbacks(applicationContext: Context) { - if (isResourcesCacheRegisteredForCallbacks) return - - if (resourcesLRUCache is ComponentCallbacks2) { - applicationContext.registerComponentCallbacks(resourcesLRUCache) - isResourcesCacheRegisteredForCallbacks = true - } else { - logger.log( - level = InternalLogger.Level.WARN, - target = InternalLogger.Target.MAINTAINER, - messageBuilder = { DOES_NOT_IMPLEMENT_COMPONENTCALLBACKS } - ) - } - } - - @MainThread - private fun registerBitmapPoolForCallbacks(applicationContext: Context) { - if (isBitmapPoolRegisteredForCallbacks) return - - applicationContext.registerComponentCallbacks(bitmapPool) - isBitmapPoolRegisteredForCallbacks = true - } - - private fun tryToDrawNewBitmap( - resources: Resources, - drawable: Drawable, - drawableWidth: Int, - drawableHeight: Int, - displayMetrics: DisplayMetrics, - imageWireframe: MobileSegment.Wireframe.ImageWireframe, - resourcesSerializerCallback: ResourcesSerializerCallback, - didCallOriginateFromFailover: Boolean - ) { - drawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = resources, - drawable = drawable, - drawableWidth = drawableWidth, - drawableHeight = drawableHeight, - displayMetrics = displayMetrics, - bitmapCreationCallback = object : BitmapCreationCallback { - override fun onReady(bitmap: Bitmap) { - Runnable { - @Suppress("ThreadSafety") // this runs inside an executor - serializeBitmap( - resources = resources, - drawable = drawable, - displayMetrics = displayMetrics, - bitmap = bitmap, - shouldCacheBitmap = true, - imageWireframe = imageWireframe, - resourcesSerializerCallback = resourcesSerializerCallback, - didCallOriginateFromFailover = didCallOriginateFromFailover - ) - }.let { - threadPoolExecutor.executeSafe("tryToDrawNewBitmap", logger, it) - } - } - - override fun onFailure() { - resourcesSerializerCallback.onReady() - } - } - ) - } - - @MainThread - private fun tryToGetBitmapFromBitmapDrawable( - resources: Resources, - drawable: Drawable, - displayMetrics: DisplayMetrics, - imageWireframe: MobileSegment.Wireframe.ImageWireframe, - resourcesSerializerCallback: ResourcesSerializerCallback - ): Bitmap? { - if (drawable is BitmapDrawable && shouldUseDrawableBitmap(drawable)) { - val bitmap = drawable.bitmap // cannot be null - we already checked in shouldUseDrawableBitmap - Runnable { - @Suppress("ThreadSafety") // this runs inside an executor - drawableUtils.createScaledBitmap( - bitmap - )?.let { scaledBitmap -> - - /** - * Check whether the scaled bitmap is the same as the original. - * Since Bitmap.createScaledBitmap will return the original bitmap if the - * requested dimensions match the dimensions of the original - */ - val shouldCacheBitmap = scaledBitmap != drawable.bitmap - - serializeBitmap( - resources = resources, - drawable = drawable, - displayMetrics = displayMetrics, - bitmap = scaledBitmap, - shouldCacheBitmap = shouldCacheBitmap, - imageWireframe = imageWireframe, - resourcesSerializerCallback = resourcesSerializerCallback, - didCallOriginateFromFailover = false - ) - } - }.let { - threadPoolExecutor.executeSafe("tryToGetBitmapFromBitmapDrawable", logger, it) - } - - // return a value to indicate that we are handling the bitmap - return bitmap - } - - return null - } - - private fun tryToGetResourceFromCache( - drawable: Drawable, - imageWireframe: MobileSegment.Wireframe.ImageWireframe, - resourcesSerializerCallback: ResourcesSerializerCallback - ): String? { - val resourceIdByteArray = resourcesLRUCache.get(drawable) ?: return null - - finalizeRecordedDataItem(resourceIdByteArray, imageWireframe) - - resourcesSerializerCallback.onReady() - - return String(resourceIdByteArray, Charsets.UTF_8) - } - - private fun finalizeRecordedDataItem( - resourceIdByteArray: ByteArray, - wireframe: MobileSegment.Wireframe.ImageWireframe - ) { - val resourceId = String(resourceIdByteArray, Charsets.UTF_8) - - wireframe.resourceId = resourceId - wireframe.isEmpty = false - } - - private fun shouldUseDrawableBitmap(drawable: BitmapDrawable): Boolean { - return drawable.bitmap != null && - !drawable.bitmap.isRecycled && - drawable.bitmap.width > 0 && - drawable.bitmap.height > 0 - } - - @MainThread - private fun registerCallbacks(applicationContext: Context) { - registerResourceLruCacheForCallbacks(applicationContext) - registerBitmapPoolForCallbacks(applicationContext) - } - - // endregion - - // region builder - internal class Builder( - private var applicationId: String, - private var recordedDataQueueHandler: DataQueueHandler = NoopDataQueueHandler(), - private var logger: InternalLogger = InternalLogger.UNBOUND, - private var threadPoolExecutor: ExecutorService = THREADPOOL_EXECUTOR, - private var bitmapPool: BitmapPool, - private var resourcesLRUCache: Cache, - private var drawableUtils: DrawableUtils = DrawableUtils( - bitmapPool = bitmapPool, - threadPoolExecutor = threadPoolExecutor, - logger = logger - ), - private var webPImageCompression: ImageCompression = WebPImageCompression(), - private var md5HashGenerator: MD5HashGenerator = MD5HashGenerator(logger) - ) { - internal fun build() = - ResourcesSerializer( - logger = logger, - threadPoolExecutor = threadPoolExecutor, - bitmapPool = bitmapPool, - resourcesLRUCache = resourcesLRUCache, - drawableUtils = drawableUtils, - webPImageCompression = webPImageCompression, - md5HashGenerator = md5HashGenerator, - recordedDataQueueHandler = recordedDataQueueHandler, - applicationId = applicationId - ) - - private companion object { - private const val THREAD_POOL_MAX_KEEP_ALIVE_MS = 5000L - private const val CORE_DEFAULT_POOL_SIZE = 1 - private const val MAX_THREAD_COUNT = 10 - - @Suppress("UnsafeThirdPartyFunctionCall") // all parameters are non-negative and queue is not null - private val THREADPOOL_EXECUTOR = ThreadPoolExecutor( - CORE_DEFAULT_POOL_SIZE, - MAX_THREAD_COUNT, - THREAD_POOL_MAX_KEEP_ALIVE_MS, - TimeUnit.MILLISECONDS, - LinkedBlockingDeque() - ) - } - } - - internal interface BitmapCreationCallback { - fun onReady(bitmap: Bitmap) - fun onFailure() - } - - // endregion -} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/WebPImageCompression.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/WebPImageCompression.kt index 9c6a83cfc6..ddbdf4487c 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/WebPImageCompression.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/WebPImageCompression.kt @@ -8,17 +8,16 @@ package com.datadog.android.sessionreplay.internal.recorder.resources import android.graphics.Bitmap import android.os.Build -import android.webkit.MimeTypeMap import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger import java.io.ByteArrayOutputStream /** * Handle webp image compression. */ -internal class WebPImageCompression : ImageCompression { - - override fun getMimeType(): String? = - MimeTypeMap.getSingleton().getMimeTypeFromExtension(WEBP_EXTENSION) +internal class WebPImageCompression( + private val logger: InternalLogger +) : ImageCompression { @WorkerThread override fun compressBitmap(bitmap: Bitmap): ByteArray { @@ -32,7 +31,14 @@ internal class WebPImageCompression : ImageCompression { @Suppress("UnsafeThirdPartyFunctionCall") bitmap.compress(imageFormat, IMAGE_QUALITY, byteArrayOutputStream) } catch (e: IllegalStateException) { - // if the bitmap was recycled while we were working on it + // probably if the bitmap was recycled while we were working on it + logger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { IMAGE_COMPRESSION_ERROR }, + e + ) + return EMPTY_BYTEARRAY } @@ -49,10 +55,11 @@ internal class WebPImageCompression : ImageCompression { companion object { private val EMPTY_BYTEARRAY = ByteArray(0) - private const val WEBP_EXTENSION = "webp" // This is the default compression for webp when writing to the output stream - // a lower quality leads to a lower filesize and worse fidelity image private const val IMAGE_QUALITY = 75 + + private const val IMAGE_COMPRESSION_ERROR = "Error while compressing the image." } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/wrappers/CanvasWrapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/wrappers/CanvasWrapper.kt index 74192b183d..f9118aef64 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/wrappers/CanvasWrapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/wrappers/CanvasWrapper.kt @@ -19,7 +19,6 @@ internal class CanvasWrapper( Canvas(bitmap) } catch (e: IllegalStateException) { // should never happen since we are passing an immutable bitmap - // TODO: REPLAY-1364 Add logs here once the sdkLogger is added logger.log( level = InternalLogger.Level.ERROR, target = InternalLogger.Target.MAINTAINER, @@ -28,7 +27,6 @@ internal class CanvasWrapper( ) null } catch (e: RuntimeException) { - // TODO: REPLAY-1364 Add logs here once the sdkLogger is added logger.log( level = InternalLogger.Level.ERROR, target = InternalLogger.Target.MAINTAINER, diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtils.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtils.kt index 18c6f9be45..33db3aeaf0 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtils.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtils.kt @@ -20,21 +20,18 @@ import androidx.annotation.MainThread import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger -import com.datadog.android.core.internal.utils.executeSafe -import com.datadog.android.sessionreplay.internal.recorder.resources.BitmapPool -import com.datadog.android.sessionreplay.internal.recorder.resources.ResourcesSerializer +import com.datadog.android.sessionreplay.internal.recorder.resources.BitmapCachesManager +import com.datadog.android.sessionreplay.internal.recorder.resources.ResourceResolver import com.datadog.android.sessionreplay.internal.recorder.wrappers.BitmapWrapper import com.datadog.android.sessionreplay.internal.recorder.wrappers.CanvasWrapper -import java.util.concurrent.ExecutorService import kotlin.math.sqrt internal class DrawableUtils( - private val bitmapWrapper: BitmapWrapper = BitmapWrapper(), - private val canvasWrapper: CanvasWrapper = CanvasWrapper(), - private val bitmapPool: BitmapPool? = null, - private val threadPoolExecutor: ExecutorService, - private val mainThreadHandler: Handler = Handler(Looper.getMainLooper()), - private val logger: InternalLogger + private val logger: InternalLogger, + private val bitmapCachesManager: BitmapCachesManager, + private val bitmapWrapper: BitmapWrapper = BitmapWrapper(logger), + private val canvasWrapper: CanvasWrapper = CanvasWrapper(logger), + private val mainThreadHandler: Handler = Handler(Looper.getMainLooper()) ) { /** @@ -42,6 +39,7 @@ internal class DrawableUtils( * be equal or less than a given size. It does so by modifying the dimensions of the * bitmap, since the file size of a bitmap can be known by the formula width*height*color depth */ + @WorkerThread internal fun createBitmapOfApproxSizeFromDrawable( resources: Resources, drawable: Drawable, @@ -50,42 +48,38 @@ internal class DrawableUtils( displayMetrics: DisplayMetrics, requestedSizeInBytes: Int = MAX_BITMAP_SIZE_BYTES_WITH_RESOURCE_ENDPOINT, config: Config = Config.ARGB_8888, - bitmapCreationCallback: ResourcesSerializer.BitmapCreationCallback + bitmapCreationCallback: ResourceResolver.BitmapCreationCallback ) { - Runnable { - @Suppress("ThreadSafety") // this runs inside an executor - createScaledBitmap( - drawableWidth, - drawableHeight, - requestedSizeInBytes, - displayMetrics, - config, - resizeBitmapCallback = object : - ResizeBitmapCallback { - override fun onSuccess(bitmap: Bitmap) { - mainThreadHandler.post { - @Suppress("ThreadSafety") // this runs on the main thread - drawOnCanvas( - resources, - bitmap, - drawable, - bitmapCreationCallback - ) - } + createScaledBitmap( + drawableWidth, + drawableHeight, + requestedSizeInBytes, + displayMetrics, + config, + resizeBitmapCallback = object : + ResizeBitmapCallback { + override fun onSuccess(bitmap: Bitmap) { + mainThreadHandler.post { + @Suppress("ThreadSafety") // this runs on the main thread + drawOnCanvas( + resources, + bitmap, + drawable, + bitmapCreationCallback + ) } + } - override fun onFailure() { - bitmapCreationCallback.onFailure() - } + override fun onFailure() { + logger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { FAILED_TO_CREATE_SCALED_BITMAP_ERROR } + ) + bitmapCreationCallback.onFailure() } - ) - }.let { - threadPoolExecutor.executeSafe( - "createBitmapOfApproxSizeFromDrawable", - logger, - it - ) - } + } + ) } @WorkerThread @@ -111,7 +105,7 @@ internal class DrawableUtils( resources: Resources, bitmap: Bitmap, drawable: Drawable, - bitmapCreationCallback: ResourcesSerializer.BitmapCreationCallback + bitmapCreationCallback: ResourceResolver.BitmapCreationCallback ) { // don't use the original drawable - it will affect the view hierarchy val newDrawable = drawable.constantState?.newDrawable(resources) @@ -187,12 +181,14 @@ internal class DrawableUtils( height: Int, config: Config ): Bitmap? = - bitmapPool?.getBitmapByProperties(width, height, config) + bitmapCachesManager.getBitmapByProperties(width, height, config) ?: bitmapWrapper.createBitmap(displayMetrics, width, height, config) internal companion object { @VisibleForTesting internal const val MAX_BITMAP_SIZE_BYTES_WITH_RESOURCE_ENDPOINT = 10 * 1024 * 1024 // 10mb private const val ARGB_8888_PIXEL_SIZE_BYTES = 4 + internal const val FAILED_TO_CREATE_SCALED_BITMAP_ERROR = + "Failed to create a scaled bitmap from the drawable" } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtils.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtils.kt index 883f9bbdd4..ea72c5ed0c 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtils.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtils.kt @@ -13,10 +13,10 @@ import android.os.Build import android.util.TypedValue import android.view.WindowManager import com.datadog.android.api.InternalLogger -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.SystemInformation import com.datadog.android.sessionreplay.internal.recorder.densityNormalized -import com.datadog.android.sessionreplay.utils.StringUtils +import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter +import com.datadog.android.sessionreplay.utils.GlobalBounds import com.google.gson.JsonObject import com.google.gson.JsonParseException import com.google.gson.JsonParser @@ -83,14 +83,14 @@ internal object MiscUtils { fun resolveSystemInformation(context: Context): SystemInformation { val screenDensity = context.resources.displayMetrics.density - val themeColorAsHexa = resolveThemeColor(context.theme)?.let { - StringUtils.formatColorAndAlphaAsHexa(it, OPAQUE_ALPHA_VALUE) + val themeColorAsHexString = resolveThemeColor(context.theme)?.let { + DefaultColorStringFormatter.formatColorAndAlphaAsHexString(it, OPAQUE_ALPHA_VALUE) } return SystemInformation( screenBounds = resolveScreenBounds(context, screenDensity), screenOrientation = context.resources.configuration.orientation, screenDensity = screenDensity, - themeColor = themeColorAsHexa + themeColor = themeColorAsHexString ) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/AndroidMDrawableToColorMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/AndroidMDrawableToColorMapper.kt new file mode 100644 index 0000000000..df227667d8 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/AndroidMDrawableToColorMapper.kt @@ -0,0 +1,33 @@ +/* + * 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.sessionreplay.utils + +//noinspection SuspiciousImport +import android.R +import android.graphics.drawable.InsetDrawable +import android.graphics.drawable.RippleDrawable +import android.os.Build +import androidx.annotation.RequiresApi + +/** + * Drawable utility object needed in the Session Replay Wireframe Mappers. + * This class is meant for internal usage so please use it carefully as it might change in time. + */ +@RequiresApi(Build.VERSION_CODES.M) +open class AndroidMDrawableToColorMapper : LegacyDrawableToColorMapper() { + + override fun resolveRippleDrawable(drawable: RippleDrawable): Int? { + // A ripple drawable can have a layer marked as mask, and which is not drawn + // We can reuse the LayerDrawable by passing a way to filter the mask layer + val maskLayerIndex = drawable.findIndexByLayerId(R.id.mask) + return resolveLayerDrawable(drawable) { idx, _ -> idx != maskLayerIndex } + } + + override fun resolveInsetDrawable(drawable: InsetDrawable): Int? { + return drawable.drawable?.let { mapDrawableToColor(it) } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/AndroidQDrawableToColorMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/AndroidQDrawableToColorMapper.kt new file mode 100644 index 0000000000..67aecb937a --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/AndroidQDrawableToColorMapper.kt @@ -0,0 +1,89 @@ +/* + * 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.sessionreplay.utils + +//noinspection SuspiciousImport +import android.graphics.BlendMode +import android.graphics.BlendModeColorFilter +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.drawable.GradientDrawable +import android.os.Build +import androidx.annotation.RequiresApi + +/** + * Drawable utility object needed in the Session Replay Wireframe Mappers. + * This class is meant for internal usage so please use it carefully as it might change in time. + */ +@RequiresApi(Build.VERSION_CODES.Q) +open class AndroidQDrawableToColorMapper : AndroidMDrawableToColorMapper() { + + override fun resolveGradientDrawable(drawable: GradientDrawable): Int? { + @Suppress("SwallowedException") + val fillPaint = try { + fillPaintField?.get(drawable) as? Paint + } catch (e: IllegalArgumentException) { + null + } catch (e: IllegalAccessException) { + null + } + + if (fillPaint == null) return null + + val colorFilter = fillPaint.colorFilter + var fillColor: Int = fillPaint.color + val fillAlpha = (fillPaint.alpha * drawable.alpha) / MAX_ALPHA_VALUE + + return if (fillAlpha == 0) { + null + } else { + if (colorFilter != null) { + fillColor = resolveBlendModeColorFilter(fillColor, colorFilter) + } + mergeColorAndAlpha(fillColor, fillAlpha) + } + } + + /** + * This is an oversimplification as the result image would only have some + * pixels with the given color, but here we're reducing a background to a single color. + * cf: https://developer.android.com/reference/android/graphics/BlendMode + * TODO RUM-3469 resolve other blend modes + */ + private fun resolveBlendModeColorFilter( + fillColor: Int, + colorFilter: ColorFilter + ): Int { + return if (colorFilter is BlendModeColorFilter) { + when (colorFilter.mode) { + in blendModesReturningBlendColor -> colorFilter.color + in blendModesReturningOriginalColor -> fillColor + else -> fillColor + } + } else { + fillColor + } + } + + companion object { + internal val blendModesReturningBlendColor = listOf( + BlendMode.SRC, + BlendMode.SRC_ATOP, + BlendMode.SRC_IN, + BlendMode.SRC_OUT, + BlendMode.SRC_OVER + ) + + internal val blendModesReturningOriginalColor = listOf( + BlendMode.DST, + BlendMode.DST_ATOP, + BlendMode.DST_IN, + BlendMode.DST_OUT, + BlendMode.DST_OVER + ) + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/AsyncJobStatusCallback.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/AsyncJobStatusCallback.kt similarity index 81% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/AsyncJobStatusCallback.kt rename to features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/AsyncJobStatusCallback.kt index 4318a4d7a4..42057790da 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/AsyncJobStatusCallback.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/AsyncJobStatusCallback.kt @@ -4,11 +4,14 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.sessionreplay.internal +package com.datadog.android.sessionreplay.utils + +import com.datadog.tools.annotation.NoOpImplementation /** * A callback to be notified when an async job starts or finishes. */ +@NoOpImplementation interface AsyncJobStatusCallback { /** diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ColorConstants.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ColorConstants.kt new file mode 100644 index 0000000000..e7f5bba8ef --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ColorConstants.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.sessionreplay.utils + +internal const val ALPHA_SHIFT_ANDROID = 24 +internal const val ALPHA_SHIFT_WEB = 8 +internal const val MAX_ALPHA_VALUE = 0xFF + +internal const val WEB_COLOR_STR_LENGTH = 8 + +internal const val MASK_ALPHA = 0xFF000000L +internal const val MASK_RGB = 0x00FFFFFFL +internal const val MASK_COLOR = 0xFFFFFFFFL diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ColorStringFormatter.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ColorStringFormatter.kt new file mode 100644 index 0000000000..46668f1283 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ColorStringFormatter.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.sessionreplay.utils + +/** + * A utility interface to convert Android/JVM colors to web hexadecimal strings. + * This interface is meant for internal usage, please use it carefully. + */ +interface ColorStringFormatter { + + /** + * Converts a color as an int to a standard web hexadecimal representation, as RGBA (e.g.: #A538AFFF). + * @param color the color value (with or without alpha in the first 8 bits) + * @return new color value as a HTML formatted hexadecimal String + */ + fun formatColorAsHexString(color: Int): String + + /** + * Converts a color as an int to a standard web hexadecimal representation, as RGBA (e.g.: #A538AFFF). + * If also overrides the color's alpha channel + * @param color the color value (with or without alpha in the first 8 bits) + * @param alpha the override alpha in a [O…255] range + * @return new color value as a HTML formatted hexadecimal String + */ + fun formatColorAndAlphaAsHexString(color: Int, alpha: Int): String +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/DefaultColorStringFormatter.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/DefaultColorStringFormatter.kt new file mode 100644 index 0000000000..59db825b92 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/DefaultColorStringFormatter.kt @@ -0,0 +1,37 @@ +/* + * 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.sessionreplay.utils + +import com.datadog.android.core.internal.utils.HEX_RADIX + +/** + * String utility methods needed in the Session Replay Wireframe Mappers. + * This class is meant for internal usage so please use it with careful as it might change in time. + */ +object DefaultColorStringFormatter : ColorStringFormatter { + + override fun formatColorAsHexString(color: Int): String { + // shift Android's ARGB to Web RGBA + val alpha = (color.toLong() and MASK_ALPHA) shr ALPHA_SHIFT_ANDROID + val colorRGBA = (color.toLong() shl ALPHA_SHIFT_WEB) or alpha + val hexString = (MASK_COLOR and colorRGBA).toString(HEX_RADIX) + return "#${hexString.padStart(WEB_COLOR_STR_LENGTH, '0')}" + } + + override fun formatColorAndAlphaAsHexString(color: Int, alpha: Int): String { + val colorRGBA = (color.toLong() shl ALPHA_SHIFT_WEB) or alpha.toLong() + + // we are going to use the `Long.toString(radius)` method to produce the hexa + // representation of the color and alpha long value because is much more faster than the + // String.format(..) approach. Based on our benchmarks, because String.format uses regular + // expressions under the hood, this approach is at least 2 times faster. + + // We remove the original alpha value from the color by masking with 0xffffffff + val hexString = (MASK_COLOR and colorRGBA).toString(HEX_RADIX) + return "#${hexString.padStart(WEB_COLOR_STR_LENGTH, '0')}" + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ViewUtils.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewBoundsResolver.kt similarity index 59% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ViewUtils.kt rename to features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewBoundsResolver.kt index 7f778c53a7..bd4705bd16 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ViewUtils.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewBoundsResolver.kt @@ -7,27 +7,15 @@ package com.datadog.android.sessionreplay.utils import android.view.View -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.densityNormalized /** * View utility methods needed in the Session Replay Wireframe Mappers. * This class is meant for internal usage so please use it with careful as it might change in time. */ -object ViewUtils { +object DefaultViewBoundsResolver : ViewBoundsResolver { - /** - * Resolves the View global bounds and normalizes them based on the screen density. - * By Global we mean that the View position will not be relative to its parent but to - * the Device screen. - * These dimensions are then normalized according with the current device screen density. - * Example: if a device has a DPI = 2, the value of the dimension or position is divided by - * 2 to get a normalized value. - * @param view as [View] - * @param pixelsDensity as the current device screen density - * @return the computed view bounds as [GlobalBounds] - */ - fun resolveViewGlobalBounds(view: View, pixelsDensity: Float): GlobalBounds { + override fun resolveViewGlobalBounds(view: View, pixelsDensity: Float): GlobalBounds { val coordinates = IntArray(2) // this will always have size >= 2 @Suppress("UnsafeThirdPartyFunctionCall") diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/UniqueIdentifierGenerator.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewIdentifierResolver.kt similarity index 58% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/UniqueIdentifierGenerator.kt rename to features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewIdentifierResolver.kt index ada5e28639..985b2dc009 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/UniqueIdentifierGenerator.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewIdentifierResolver.kt @@ -13,24 +13,18 @@ import java.security.SecureRandom * Unique Identifier Generator. * This class is meant for internal usage so please use it with careful as it might change in time. */ -object UniqueIdentifierGenerator { +object DefaultViewIdentifierResolver : ViewIdentifierResolver { internal const val DATADOG_UNIQUE_IDENTIFIER_KEY_PREFIX = "DATADOG_UNIQUE_IDENTIFIER_" private val secureRandom = SecureRandom() - /** - * Generates a persistent unique identifier for a virtual child view based on its unique - * name and its physical parent. The identifier will only be created once and persisted in - * the parent [View] tag to provide consistency. In case there was already a value with the - * same key in the tags and this was used by a different party we will try to use this value - * as identifier if it's a [Long], in other case we will return null. This last scenario is - * highly unlikely but we are doing this in order to safely treat possible collisions with - * client tags. - * @param parent [View] of this virtual child - * @param childName as the unique name of the virtual child - * @return the unique identifier as [Long] or null if the identifier could not be created - */ - fun resolveChildUniqueIdentifier(parent: View, childName: String): Long? { + override fun resolveViewId(view: View): Long { + // we will use the System.identityHashcode in here which returns a consistent + // value for an instance even when it is mutable + return System.identityHashCode(view).toLong() + } + + override fun resolveChildUniqueIdentifier(parent: View, childName: String): Long? { val key = (DATADOG_UNIQUE_IDENTIFIER_KEY_PREFIX + childName).hashCode() val uniqueIdentifier = parent.getTag(key) return if (uniqueIdentifier != null) { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/DrawableToColorMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/DrawableToColorMapper.kt new file mode 100644 index 0000000000..82c47ee5e8 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/DrawableToColorMapper.kt @@ -0,0 +1,40 @@ +/* + * 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.sessionreplay.utils + +import android.graphics.drawable.Drawable +import android.os.Build + +/** + * A utility interface to convert a [Drawable] to a meaningful color. + * This interface is meant for internal usage, please use it carefully. + */ +fun interface DrawableToColorMapper { + + /** + * Maps the drawable to its meaningful color, or null if the drawable is mostly invisible. + * @param drawable the drawable to convert + * @return the color as an Int (in 0xAARRGGBB order), or null if the drawable is mostly invisible + */ + fun mapDrawableToColor(drawable: Drawable): Int? + + companion object { + /** + * Provides a default implementation. + * @return a default implementation based on the device API level + */ + fun getDefault(): DrawableToColorMapper { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + AndroidQDrawableToColorMapper() + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + AndroidMDrawableToColorMapper() + } else { + LegacyDrawableToColorMapper() + } + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/GlobalBounds.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/GlobalBounds.kt similarity index 93% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/GlobalBounds.kt rename to features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/GlobalBounds.kt index 58ad754a00..fea677ba29 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/GlobalBounds.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/GlobalBounds.kt @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.sessionreplay.internal.recorder +package com.datadog.android.sessionreplay.utils /** * Defines the dimension and positions in Global coordinates for a geometry. By Global we mean that diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ImageWireframeHelper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ImageWireframeHelper.kt new file mode 100644 index 0000000000..45e8c964fe --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ImageWireframeHelper.kt @@ -0,0 +1,67 @@ +/* + * 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.sessionreplay.utils + +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.TextView +import com.datadog.android.sessionreplay.internal.recorder.MappingContext +import com.datadog.android.sessionreplay.model.MobileSegment + +/** + * A Helper to handle capturing images in Session replay wireframes. + */ +interface ImageWireframeHelper { + + /** + * Asks the helper to create an image wireframe, and process the provided drawable in the background. + * @param view the view owning the drawable + * @param currentWireframeIndex the index of the wireframe in the list of wireframes for the view + * @param x the x position of the image + * @param y the y position of the image + * @param width the width of the image + * @param height the width of the image + * @param usePIIPlaceholder whether to replace the image content with a placeholder when we suspect it contains PII + * @param drawable the drawable to capture + * @param asyncJobStatusCallback the callback for the async capture process + * @param clipping the bounds of the image that are actually visible + * @param shapeStyle provides a custom shape (e.g. rounded corners) to the image wireframe + * @param border provides a custom border to the image wireframe + * @param prefix a prefix identifying the drawable in the parent view's context + */ + // TODO RUM-0000 limit the number of params to this function + fun createImageWireframe( + view: View, + currentWireframeIndex: Int, + x: Long, + y: Long, + width: Int, + height: Int, + usePIIPlaceholder: Boolean, + drawable: Drawable, + asyncJobStatusCallback: AsyncJobStatusCallback, + clipping: MobileSegment.WireframeClip? = null, + shapeStyle: MobileSegment.ShapeStyle? = null, + border: MobileSegment.ShapeBorder? = null, + prefix: String? = DRAWABLE_CHILD_NAME + ): MobileSegment.Wireframe? + + /** + * Creates the wireframes for the compound drawables in a [TextView]. + * @param + */ + fun createCompoundDrawableWireframes( + textView: TextView, + mappingContext: MappingContext, + prevWireframeIndex: Int, + asyncJobStatusCallback: AsyncJobStatusCallback + ): MutableList + + companion object { + internal const val DRAWABLE_CHILD_NAME = "drawable" + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapper.kt new file mode 100644 index 0000000000..a141942f88 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapper.kt @@ -0,0 +1,153 @@ +/* + * 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.sessionreplay.utils + +//noinspection SuspiciousImport +import android.graphics.Paint +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.InsetDrawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.RippleDrawable +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.FeatureSdkCore + +/** + * Drawable utility object needed in the Session Replay Wireframe Mappers. + * This class is meant for internal usage so please use it carefully as it might change in time. + */ +open class LegacyDrawableToColorMapper : DrawableToColorMapper { + + override fun mapDrawableToColor(drawable: Drawable): Int? { + val result = when (drawable) { + is ColorDrawable -> resolveColorDrawable(drawable) + is RippleDrawable -> resolveRippleDrawable(drawable) + is LayerDrawable -> resolveLayerDrawable(drawable) + is InsetDrawable -> resolveInsetDrawable(drawable) + is GradientDrawable -> resolveGradientDrawable(drawable) + else -> { + null + } + } + + return result + } + + /** + * Resolves the color from a [ColorDrawable]. + * @param drawable the color drawable + * @return the color to map to or null if not applicable + */ + protected open fun resolveColorDrawable(drawable: ColorDrawable): Int? { + return mergeColorAndAlpha(drawable.color, drawable.alpha) + } + + /** + * Resolves the color from a [RippleDrawable]. + * @param drawable the color drawable + * @return the color to map to or null if not applicable + */ + protected open fun resolveRippleDrawable(drawable: RippleDrawable): Int? { + return resolveLayerDrawable(drawable) + } + + /** + * Resolves the color from a [LayerDrawable]. + * @param drawable the color drawable + * @param predicate a predicate to filter which ayers should be taken into account (default: accept all layers) + * @return the color to map to or null if not applicable + */ + protected open fun resolveLayerDrawable( + drawable: LayerDrawable, + predicate: (Int, Drawable) -> Boolean = { _, _ -> true } + ): Int? { + return (0 until drawable.numberOfLayers).map { idx -> + @Suppress("UnsafeThirdPartyFunctionCall") // layer index can't be out of bounds here + val childDrawable = drawable.getDrawable(idx) + if (childDrawable != null && predicate(idx, childDrawable)) { + mapDrawableToColor(childDrawable) + } else { + null + } + }.firstOrNull { it != null } + } + + /** + * Resolves the color from a [GradientDrawable]. + * @param drawable the color drawable + * @return the color to map to or null if not applicable + */ + protected open fun resolveGradientDrawable(drawable: GradientDrawable): Int? { + val fillPaint = try { + fillPaintField?.get(drawable) as? Paint + } catch (e: IllegalArgumentException) { + (Datadog.getInstance() as? FeatureSdkCore)?.internalLogger?.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { "Unable to read GradientDrawable.mFillPaint field through reflection" }, + e + ) + null + } catch (e: IllegalAccessException) { + (Datadog.getInstance() as? FeatureSdkCore)?.internalLogger?.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { "Unable to read GradientDrawable.mFillPaint field through reflection" }, + e + ) + null + } + + if (fillPaint == null) return null + + val fillColor: Int = fillPaint.color + val fillAlpha = (fillPaint.alpha * drawable.alpha) / MAX_ALPHA_VALUE + + return if (fillAlpha == 0) { + null + } else { + // TODO RUM-3469 resolve other color filter types + mergeColorAndAlpha(fillColor, fillAlpha) + } + } + + /** + * Resolves the color from an [InsetDrawable]. + * @param drawable the color drawable + * @return the color to map to or null if not applicable + */ + protected open fun resolveInsetDrawable(drawable: InsetDrawable): Int? { + return null + } + + /** + * Merges a color (as an (A)RGB int) with an alpha value, replacing the alpha of the original color. + * @param color a color (as an (A)RGB int) + * @param alpha the alpha (between 0 and 255) + * @return a color with the RGB component matching the input color, and alpha component matching the alpha input + */ + protected fun mergeColorAndAlpha(color: Int, alpha: Int): Int { + return ((color.toLong() and MASK_COLOR) or (alpha.toLong() shl ALPHA_SHIFT_ANDROID)).toInt() + } + + companion object { + @Suppress("PrivateAPI", "SwallowedException", "TooGenericExceptionCaught") + internal val fillPaintField = try { + GradientDrawable::class.java.getDeclaredField("mFillPaint").apply { + this.isAccessible = true + } + } catch (e: NoSuchFieldException) { + null + } catch (e: SecurityException) { + null + } catch (e: NullPointerException) { + null + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/StringUtils.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/StringUtils.kt deleted file mode 100644 index 45041708a1..0000000000 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/StringUtils.kt +++ /dev/null @@ -1,45 +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.sessionreplay.utils - -/** - * String utility methods needed in the Session Replay Wireframe Mappers. - * This class is meant for internal usage so please use it with careful as it might change in time. - */ -object StringUtils { - - /** - * Removes the alpha value from the original color and adds the provided alpha at the end of - * the it. It will then return the new color value as a HTML formatted hexa String (R,G,B,A) - * @param color as the color value with or without alpha in the first 8 bits - * @param alpha as Int. It can take values from 0 to 255. - * @return new color value as a HTML formatted hexa String (R,G,B,A) - */ - @Suppress("MagicNumber") - fun formatColorAndAlphaAsHexa(color: Int, alpha: Int): String { - // we shift left 8 bits to make room for alpha - val colorAndAlpha = (color.toLong().shl(8)).or(alpha.toLong()) - // we are going to use the `Long.toString(radius)` method to produce the hexa - // representation of the color and alpha long value because is much more faster than the - // String.format(..) approach. Based on our benchmarks, because String.format uses regular - // expressions under the hood, this approach is at least 2 times faster. - - // We remove the original alpha value from the color by masking with 0xffffffff - val colorAndAlphaAsHexa = (0xffffffff and colorAndAlpha).toString(16) - var requiredLength = 9 - - @Suppress("UnsafeThirdPartyFunctionCall") // argument is not negative - val sb = StringBuilder(requiredLength) - sb.append("#") - requiredLength-- - repeat(requiredLength - colorAndAlphaAsHexa.length) { - sb.append('0') - } - sb.append(colorAndAlphaAsHexa) - return sb.toString() - } -} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ViewBoundsResolver.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ViewBoundsResolver.kt new file mode 100644 index 0000000000..5a63d80af5 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ViewBoundsResolver.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.sessionreplay.utils + +import android.view.View + +/** + * A utility interface to extract a [View]'s bounds relative to the device's screen, and scaled according to + * the screen's density. + * This interface is meant for internal usage, please use it carefully. + */ +fun interface ViewBoundsResolver { + /** + * Resolves the View bounds in device space, and normalizes them based on the screen density. + * These dimensions are then normalized according with the current device screen density. + * Example: if a device has a DPI = 2, the value of the dimension or position is divided by + * 2 to get a normalized value. + * @param view the [View] + * @param pixelsDensity the current device screen density + * @return the computed view bounds + */ + // TODO RUM-0000 return an array of primitives here instead of creating an object. + // This method is being called too often every time we take a screen snapshot + // and we might want to avoid creating too many instances. + fun resolveViewGlobalBounds(view: View, pixelsDensity: Float): GlobalBounds +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ViewIdentifierResolver.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ViewIdentifierResolver.kt new file mode 100644 index 0000000000..11dec623ea --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ViewIdentifierResolver.kt @@ -0,0 +1,37 @@ +/* + * 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.sessionreplay.utils + +import android.view.View + +/** + * A utility interface to assign a unique id to the child of a View. + * This interface is meant for internal usage, please use it carefully. + */ +interface ViewIdentifierResolver { + + /** + * Resolves a persistent, unique id for the given view. + * @param view the view + * @return an identifier unquely mapping a view to a wireframe, allowing accurate diffs + */ + fun resolveViewId(view: View): Long + + /** + * Generates a persistent unique identifier for a virtual child view based on its unique + * name and its physical parent. The identifier will only be created once and persisted in + * the parent [View] tag to provide consistency. In case there was already a value with the + * same key in the tags and this was used by a different party we will try to use this value + * as identifier if it's a [Long], in other case we will return null. This last scenario is + * highly unlikely but we are doing this in order to safely treat possible collisions with + * client tags. + * @param parent the parent [View] of the virtual child + * @param childName the unique name of the virtual child + * @return the unique identifier as [Long] or null if the identifier could not be created + */ + fun resolveChildUniqueIdentifier(parent: View, childName: String): Long? +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacyTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacyTest.kt index 0a02451d76..f9c47d691d 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacyTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacyTest.kt @@ -18,9 +18,7 @@ import android.widget.SeekBar import android.widget.TextView import android.widget.Toolbar import androidx.appcompat.widget.SwitchCompat -import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler import com.datadog.android.sessionreplay.internal.recorder.mapper.ButtonMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckBoxMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckedTextViewMapper @@ -42,7 +40,6 @@ import com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.UnsupportedViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper import com.datadog.tools.unit.setStaticValue -import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -53,12 +50,10 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import org.junit.runners.Parameterized -import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.mock import org.mockito.quality.Strictness -import java.util.UUID import java.util.stream.Stream import androidx.appcompat.widget.Toolbar as AppCompatToolbar @@ -77,15 +72,6 @@ internal class SessionReplayPrivacyTest { */ private val origApiLevel = Build.VERSION.SDK_INT - @Mock - lateinit var mockLogger: InternalLogger - - @Forgery - lateinit var fakeApplicationId: UUID - - @Mock - lateinit var mockRecordedDataQueueHandler: RecordedDataQueueHandler - @AfterEach fun teardown() { setApiLevel(origApiLevel) @@ -103,21 +89,9 @@ internal class SessionReplayPrivacyTest { // When val actualMappers = when (maskLevel) { - SessionReplayPrivacy.ALLOW.toString() -> SessionReplayPrivacy.ALLOW.mappers( - mockLogger, - fakeApplicationId.toString(), - mockRecordedDataQueueHandler - ) - SessionReplayPrivacy.MASK.toString() -> SessionReplayPrivacy.MASK.mappers( - mockLogger, - fakeApplicationId.toString(), - mockRecordedDataQueueHandler - ) - SessionReplayPrivacy.MASK_USER_INPUT.toString() -> SessionReplayPrivacy.MASK_USER_INPUT.mappers( - mockLogger, - fakeApplicationId.toString(), - mockRecordedDataQueueHandler - ) + SessionReplayPrivacy.ALLOW.toString() -> SessionReplayPrivacy.ALLOW.mappers() + SessionReplayPrivacy.MASK.toString() -> SessionReplayPrivacy.MASK.mappers() + SessionReplayPrivacy.MASK_USER_INPUT.toString() -> SessionReplayPrivacy.MASK_USER_INPUT.mappers() else -> throw IllegalArgumentException("Unknown masking level") } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/GlobalBoundsForgeryFactory.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/GlobalBoundsForgeryFactory.kt index 09ef222571..a254e76d3a 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/GlobalBoundsForgeryFactory.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/GlobalBoundsForgeryFactory.kt @@ -6,7 +6,7 @@ package com.datadog.android.sessionreplay.forge -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds +import com.datadog.android.sessionreplay.utils.GlobalBounds import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/MappingContextForgeryFactory.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/MappingContextForgeryFactory.kt index 2b8d690e90..0c844a48b7 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/MappingContextForgeryFactory.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/MappingContextForgeryFactory.kt @@ -9,11 +9,13 @@ package com.datadog.android.sessionreplay.forge import com.datadog.android.sessionreplay.internal.recorder.MappingContext import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory +import org.mockito.Mockito.mock internal class MappingContextForgeryFactory : ForgeryFactory { override fun getForgery(forge: Forge): MappingContext { return MappingContext( systemInformation = forge.getForgery(), + imageWireframeHelper = mock(), hasOptionSelectorParent = forge.aBool() ) } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/SystemInformationForgeryFactory.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/SystemInformationForgeryFactory.kt index 531d1209ff..5baf0ae370 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/SystemInformationForgeryFactory.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/SystemInformationForgeryFactory.kt @@ -7,8 +7,8 @@ package com.datadog.android.sessionreplay.forge import android.content.res.Configuration -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.SystemInformation +import com.datadog.android.sessionreplay.utils.GlobalBounds import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolverTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolverTest.kt index 90ef40909e..c9d8aac9db 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolverTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolverTest.kt @@ -316,7 +316,7 @@ internal class MutationResolverTest { val previousId = if (index > 0) fakeCurrentSnapshot[index - 1].id() else null expectedAdds.add( com.datadog.android.sessionreplay.model.MobileSegment.Add - (previousId, wireframe) + (previousId, wireframe) ) } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/RecordedDataProcessorTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/RecordedDataProcessorTest.kt index 5be1f5fa5a..ee32128097 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/RecordedDataProcessorTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/RecordedDataProcessorTest.kt @@ -1,4 +1,4 @@ - /* +/* * 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. @@ -1271,22 +1271,22 @@ internal class RecordedDataProcessorTest { resourceData: ByteArray, usedContext: RecordedQueuedItemContext = currentRecordedQueuedItemContext ): ResourceRecordedDataQueueItem = ResourceRecordedDataQueueItem( - recordedQueuedItemContext = usedContext, - resourceData = resourceData, - applicationId = fakeRumContext.applicationId, - identifier = fakeIdentifier - ) + recordedQueuedItemContext = usedContext, + resourceData = resourceData, + applicationId = fakeRumContext.applicationId, + identifier = fakeIdentifier + ) private fun createSnapshotItem( snapshot: List, systemInformation: SystemInformation = fakeSystemInformation, usedContext: RecordedQueuedItemContext = currentRecordedQueuedItemContext ): SnapshotRecordedDataQueueItem = SnapshotRecordedDataQueueItem( - usedContext, - systemInformation = systemInformation - ).apply { - this.nodes = snapshot - } + usedContext, + systemInformation = systemInformation + ).apply { + this.nodes = snapshot + } private fun createTouchEventItem( touchEvent: List, diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtilsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtilsTest.kt index 88d7e80b37..16f11e2afe 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtilsTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtilsTest.kt @@ -13,6 +13,7 @@ import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -112,8 +113,8 @@ internal class WireframeUtilsTest { whenever(mockBoundsUtils.resolveBounds(fakeWireframe)).thenReturn(this) } val fakeParentBounds = fakeWireframeBounds.copy( - left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left), - top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top), + left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left.coerceAtLeast(1)), + top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top.coerceAtLeast(1)), right = fakeWireframeBounds.right + forge.aLong(min = 0, max = fakeWireframeBounds.right), bottom = fakeWireframeBounds.bottom + @@ -145,9 +146,13 @@ internal class WireframeUtilsTest { val fakeWireframeBounds: WireframeBounds = forge.getForgery().apply { whenever(mockBoundsUtils.resolveBounds(fakeWireframe)).thenReturn(this) } + assumeTrue(fakeWireframeBounds.left > 0) + assumeTrue(fakeWireframeBounds.top > 0) + assumeTrue(fakeWireframeBounds.right > 0) + assumeTrue(fakeWireframeBounds.bottom > 0) val fakeParentBounds = fakeWireframeBounds.copy( - left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left), - top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top), + left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left.coerceAtLeast(1)), + top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top.coerceAtLeast(1)), right = fakeWireframeBounds.right + forge.aLong(min = 0, max = fakeWireframeBounds.right), bottom = fakeWireframeBounds.bottom + @@ -180,8 +185,8 @@ internal class WireframeUtilsTest { whenever(mockBoundsUtils.resolveBounds(fakeWireframe)).thenReturn(this) } val fakeParentBounds = fakeWireframeBounds.copy( - left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left), - top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top), + left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left.coerceAtLeast(1)), + top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top.coerceAtLeast(1)), right = fakeWireframeBounds.right + forge.aLong(min = 0, max = fakeWireframeBounds.right), bottom = fakeWireframeBounds.bottom + @@ -214,8 +219,8 @@ internal class WireframeUtilsTest { whenever(mockBoundsUtils.resolveBounds(fakeWireframe)).thenReturn(this) } val fakeParentBounds = fakeWireframeBounds.copy( - left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left), - top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top), + left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left.coerceAtLeast(1)), + top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top.coerceAtLeast(1)), right = fakeWireframeBounds.right + forge.aLong(min = 0, max = fakeWireframeBounds.right), bottom = fakeWireframeBounds.bottom + @@ -248,8 +253,8 @@ internal class WireframeUtilsTest { whenever(mockBoundsUtils.resolveBounds(fakeWireframe)).thenReturn(this) } val fakeParentBounds = fakeWireframeBounds.copy( - left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left), - top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top), + left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left.coerceAtLeast(1)), + top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top.coerceAtLeast(1)), right = fakeWireframeBounds.right + forge.aLong(min = 0, max = fakeWireframeBounds.right), bottom = fakeWireframeBounds.bottom + @@ -283,8 +288,8 @@ internal class WireframeUtilsTest { whenever(mockBoundsUtils.resolveBounds(fakeWireframe)).thenReturn(this) } val fakeParentBounds = fakeWireframeBounds.copy( - left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left), - top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top), + left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left.coerceAtLeast(1)), + top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top.coerceAtLeast(1)), right = fakeWireframeBounds.right + forge.aLong(min = 0, max = fakeWireframeBounds.right), bottom = fakeWireframeBounds.bottom + @@ -311,8 +316,8 @@ internal class WireframeUtilsTest { whenever(mockBoundsUtils.resolveBounds(fakeWireframe)).thenReturn(this) } val fakeParentBounds = fakeWireframeBounds.copy( - left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left), - top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top), + left = fakeWireframeBounds.left - forge.aLong(min = 0, max = fakeWireframeBounds.left.coerceAtLeast(1)), + top = fakeWireframeBounds.top - forge.aLong(min = 0, max = fakeWireframeBounds.top.coerceAtLeast(1)), right = fakeWireframeBounds.right + forge.aLong(min = 0, max = fakeWireframeBounds.right), bottom = fakeWireframeBounds.bottom + diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducerTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducerTest.kt index dd753b4a1e..13d35e5931 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducerTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducerTest.kt @@ -11,6 +11,7 @@ import android.view.ViewGroup import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.utils.ImageWireframeHelper import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration @@ -58,9 +59,16 @@ internal class SnapshotProducerTest { @Mock lateinit var mockOptionSelectorDetector: DefaultOptionSelectorDetector + @Mock + lateinit var mockImageWireframeHelper: ImageWireframeHelper + @BeforeEach fun `set up`() { - testedSnapshotProducer = SnapshotProducer(mockTreeViewTraversal, mockOptionSelectorDetector) + testedSnapshotProducer = SnapshotProducer( + mockImageWireframeHelper, + mockTreeViewTraversal, + mockOptionSelectorDetector + ) } @Test diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewUtilsInternalTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewUtilsInternalTest.kt index 8b356fe186..47a0aad72a 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewUtilsInternalTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewUtilsInternalTest.kt @@ -13,7 +13,7 @@ import android.widget.TextView import androidx.appcompat.widget.ActionBarContextView import androidx.appcompat.widget.Toolbar import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper +import com.datadog.android.sessionreplay.internal.recorder.resources.DefaultImageWireframeHelper import com.datadog.tools.unit.extensions.ApiLevelExtension import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.IntForgery @@ -256,7 +256,7 @@ internal class ViewUtilsInternalTest { mockTextView, mockDrawable, 0f, - ImageWireframeHelper.CompoundDrawablePositions.LEFT + DefaultImageWireframeHelper.CompoundDrawablePositions.LEFT ) // Then @@ -276,7 +276,7 @@ internal class ViewUtilsInternalTest { mockTextView, mockDrawable, 0f, - ImageWireframeHelper.CompoundDrawablePositions.TOP + DefaultImageWireframeHelper.CompoundDrawablePositions.TOP ) // Then @@ -298,7 +298,7 @@ internal class ViewUtilsInternalTest { mockTextView, mockDrawable, 0f, - ImageWireframeHelper.CompoundDrawablePositions.RIGHT + DefaultImageWireframeHelper.CompoundDrawablePositions.RIGHT ) // Then @@ -320,7 +320,7 @@ internal class ViewUtilsInternalTest { mockTextView, mockDrawable, 0f, - ImageWireframeHelper.CompoundDrawablePositions.BOTTOM + DefaultImageWireframeHelper.CompoundDrawablePositions.BOTTOM ) // Then diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseCheckBoxMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseCheckBoxMapperTest.kt index 8cd86dd0c6..6cf503592d 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseCheckBoxMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseCheckBoxMapperTest.kt @@ -10,18 +10,16 @@ import android.graphics.drawable.Drawable import android.os.Build import android.widget.CheckBox import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.GlobalBounds import com.datadog.tools.unit.annotations.TestTargetApi import com.datadog.tools.unit.extensions.ApiLevelExtension import fr.xgouchet.elmyr.annotation.FloatForgery import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -33,6 +31,7 @@ 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.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -49,9 +48,6 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { lateinit var testedCheckBoxMapper: CheckBoxMapper - @Mock - lateinit var mockuniqueIdentifierGenerator: UniqueIdentifierGenerator - @Mock lateinit var mockTextWireframeMapper: TextViewMapper @@ -61,9 +57,6 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { @LongForgery var fakeGeneratedIdentifier: Long = 0L - @Mock - lateinit var mockViewUtils: ViewUtils - @Forgery lateinit var fakeViewGlobalBounds: GlobalBounds @@ -72,6 +65,9 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { @IntForgery(min = 0, max = 0xffffff) var fakeCurrentTextColor: Int = 0 + @StringForgery(regex = "#[0-9A-F]{8}") + lateinit var fakeCurrentTextColorString: String + @FloatForgery(min = 1f, max = 100f) var fakeTextSize: Float = 1f @@ -83,22 +79,29 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { mockCheckBox = mock { whenever(it.textSize).thenReturn(fakeTextSize) whenever(it.currentTextColor).thenReturn(fakeCurrentTextColor) + whenever(it.alpha) doReturn 1f } whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockCheckBox, CheckableTextViewMapper.CHECKABLE_KEY_NAME ) ).thenReturn(fakeGeneratedIdentifier) + whenever(mockTextWireframeMapper.map(eq(mockCheckBox), eq(fakeMappingContext), any())) .thenReturn(fakeTextWireframes) + whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockCheckBox, fakeMappingContext.systemInformation.screenDensity ) - ) - .thenReturn(fakeViewGlobalBounds) + ).thenReturn(fakeViewGlobalBounds) + + whenever( + mockColorStringFormatter.formatColorAndAlphaAsHexString(fakeCurrentTextColor, OPAQUE_ALPHA_VALUE) + ) doReturn fakeCurrentTextColorString + testedCheckBoxMapper = setupTestedMapper() } @@ -122,12 +125,7 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { } whenever(mockCheckBox.buttonDrawable).thenReturn(mockDrawable) whenever(mockCheckBox.isChecked).thenReturn(true) - val expectedCheckBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) - val checkBoxSize = - resolveCheckBoxSize(fakeIntrinsicDrawableHeight.toLong()) + val checkBoxSize = resolveCheckBoxSize(fakeIntrinsicDrawableHeight.toLong()) val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( id = fakeGeneratedIdentifier, x = fakeViewGlobalBounds.x + CheckableCompoundButtonMapper.MIN_PADDING_IN_PX @@ -136,16 +134,17 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedCheckBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ), - shapeStyle = expectedCheckedShapeStyle(expectedCheckBoxColor) + shapeStyle = expectedCheckedShapeStyle(fakeCurrentTextColorString) ) // When val resolvedWireframes = testedCheckBoxMapper.map( mockCheckBox, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -161,10 +160,6 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { } whenever(mockCheckBox.buttonDrawable).thenReturn(mockDrawable) whenever(mockCheckBox.isChecked).thenReturn(false) - val expectedCheckBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val checkBoxSize = resolveCheckBoxSize(fakeIntrinsicDrawableHeight.toLong()) val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( @@ -175,7 +170,7 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedCheckBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ), shapeStyle = null @@ -184,7 +179,8 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { // When val resolvedWireframes = testedCheckBoxMapper.map( mockCheckBox, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -195,10 +191,6 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { fun `M resolve the checkbox as ShapeWireframe W map() { checked }`() { // Given whenever(mockCheckBox.isChecked).thenReturn(true) - val expectedCheckBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val checkBoxSize = resolveCheckBoxSize(CheckableCompoundButtonMapper.DEFAULT_CHECKABLE_HEIGHT_IN_PX) val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( @@ -209,16 +201,17 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedCheckBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ), - shapeStyle = expectedCheckedShapeStyle(expectedCheckBoxColor) + shapeStyle = expectedCheckedShapeStyle(fakeCurrentTextColorString) ) // When val resolvedWireframes = testedCheckBoxMapper.map( mockCheckBox, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -229,10 +222,6 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { fun `M resolve the checkbox as ShapeWireframe W map() { not checked }`() { // Given whenever(mockCheckBox.isChecked).thenReturn(false) - val expectedCheckBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val checkBoxSize = resolveCheckBoxSize(CheckableCompoundButtonMapper.DEFAULT_CHECKABLE_HEIGHT_IN_PX) val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( @@ -243,7 +232,7 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedCheckBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ), shapeStyle = null @@ -252,7 +241,8 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { // When val resolvedWireframes = testedCheckBoxMapper.map( mockCheckBox, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -263,7 +253,7 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { fun `M ignore the checkbox W map() { unique id could not be generated }`() { // Given whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockCheckBox, CheckableTextViewMapper.CHECKABLE_KEY_NAME ) @@ -272,7 +262,8 @@ internal abstract class BaseCheckBoxMapperTest : BaseWireframeMapperTest() { // When val resolvedWireframes = testedCheckBoxMapper.map( mockCheckBox, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseCheckedTextViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseCheckedTextViewMapperTest.kt index 9d1a0691e5..1a0ba28cf9 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseCheckedTextViewMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseCheckedTextViewMapperTest.kt @@ -10,17 +10,15 @@ import android.content.res.ColorStateList import android.graphics.drawable.Drawable import android.widget.CheckedTextView import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.GlobalBounds import com.datadog.tools.unit.extensions.ApiLevelExtension import fr.xgouchet.elmyr.annotation.FloatForgery import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -32,6 +30,8 @@ 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.anyOrNull +import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -48,9 +48,6 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( lateinit var testedCheckedTextWireframeMapper: CheckedTextViewMapper - @Mock - lateinit var mockuniqueIdentifierGenerator: UniqueIdentifierGenerator - @Mock lateinit var mockTextWireframeMapper: TextViewMapper @@ -60,9 +57,6 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( @LongForgery var fakeGeneratedIdentifier: Long = 0L - @Mock - lateinit var mockViewUtils: ViewUtils - @Forgery lateinit var fakeViewGlobalBounds: GlobalBounds @@ -92,6 +86,9 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( @IntForgery(min = 0, max = 0xffffff) var fakeCurrentTextColor: Int = 0 + @StringForgery(regex = "#[0-9A-F]{8}") + lateinit var fakeCurrentTextColorString: String + @FloatForgery(min = 1f) var fakeTextSize: Float = 1f @@ -108,26 +105,30 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( whenever(mockCheckedTextView.currentTextColor).thenReturn(fakeCurrentTextColor) whenever(mockCheckMarkTintList.defaultColor).thenReturn(fakeCheckMarkTintColor) whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockCheckedTextView, CheckableTextViewMapper.CHECKABLE_KEY_NAME ) ).thenReturn(fakeGeneratedIdentifier) + whenever( mockTextWireframeMapper.map( eq(mockCheckedTextView), eq(fakeMappingContext), any() ) - ) - .thenReturn(fakeTextWireframes) + ).thenReturn(fakeTextWireframes) + whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockCheckedTextView, fakeMappingContext.systemInformation.screenDensity ) - ) - .thenReturn(fakeViewGlobalBounds) + ).thenReturn(fakeViewGlobalBounds) + + whenever( + mockColorStringFormatter.formatColorAndAlphaAsHexString(fakeCurrentTextColor, OPAQUE_ALPHA_VALUE) + ) doReturn fakeCurrentTextColorString testedCheckedTextWireframeMapper = setupTestedMapper() } @@ -146,10 +147,6 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( fun `M resolve the checkbox as ShapeWireframe W map() { text checked }`() { // Given whenever(mockCheckedTextView.isChecked).thenReturn(true) - val expectedCheckBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val checkBoxSize = resolveCheckBoxSize() val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( id = fakeGeneratedIdentifier, @@ -159,16 +156,17 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedCheckBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ), - shapeStyle = expectedCheckedShapeStyle(expectedCheckBoxColor) + shapeStyle = expectedCheckedShapeStyle(fakeCurrentTextColorString) ) // When val resolvedWireframes = testedCheckedTextWireframeMapper.map( mockCheckedTextView, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -179,10 +177,6 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( fun `M resolve the checkbox as ShapeWireframe W map() { text not checked }`() { // Given whenever(mockCheckedTextView.isChecked).thenReturn(false) - val expectedCheckBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val checkBoxSize = resolveCheckBoxSize() val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( id = fakeGeneratedIdentifier, @@ -192,7 +186,7 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedCheckBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ), shapeStyle = null @@ -201,7 +195,8 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( // When val resolvedWireframes = testedCheckedTextWireframeMapper.map( mockCheckedTextView, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -213,10 +208,6 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( // Given whenever(mockCheckedTextView.checkMarkDrawable).thenReturn(null) whenever(mockCheckedTextView.isChecked).thenReturn(false) - val expectedCheckBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( id = fakeGeneratedIdentifier, x = fakeViewGlobalBounds.x + fakeViewGlobalBounds.width - @@ -225,7 +216,7 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( width = 0, height = 0, border = MobileSegment.ShapeBorder( - color = expectedCheckBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ), shapeStyle = null @@ -234,7 +225,8 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( // When val resolvedWireframes = testedCheckedTextWireframeMapper.map( mockCheckedTextView, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -245,10 +237,10 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( fun `M resolve the checkbox as ShapeWireframe W map() { checkMarkTintList available }`() { // Given whenever(mockCheckedTextView.checkMarkTintList).thenReturn(mockCheckMarkTintList) - val expectedCheckBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCheckMarkTintColor, - OPAQUE_ALPHA_VALUE - ) + whenever( + mockColorStringFormatter.formatColorAndAlphaAsHexString(eq(mockCheckMarkTintList.defaultColor), anyOrNull()) + ).thenReturn(fakeCurrentTextColorString) + val checkBoxSize = resolveCheckBoxSize() val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( id = fakeGeneratedIdentifier, @@ -258,7 +250,7 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedCheckBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ) ) @@ -266,7 +258,8 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( // When val resolvedWireframes = testedCheckedTextWireframeMapper.map( mockCheckedTextView, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -277,10 +270,6 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( fun `M resolve the checkbox as ShapeWireframe W map() { checkMarkTintList is null }`() { // Given whenever(mockCheckedTextView.checkMarkTintList).thenReturn(null) - val expectedCheckBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val checkBoxSize = resolveCheckBoxSize() val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( id = fakeGeneratedIdentifier, @@ -290,7 +279,7 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedCheckBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ) ) @@ -298,7 +287,8 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( // When val resolvedWireframes = testedCheckedTextWireframeMapper.map( mockCheckedTextView, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -308,10 +298,6 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( @Test fun `M resolve the checkbox as ShapeWireframe W map() { checkMarkTintList not available }`() { // Given - val expectedCheckBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val checkBoxSize = resolveCheckBoxSize() val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( id = fakeGeneratedIdentifier, @@ -321,7 +307,7 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedCheckBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ) ) @@ -329,7 +315,8 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( // When val resolvedWireframes = testedCheckedTextWireframeMapper.map( mockCheckedTextView, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -340,7 +327,7 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( fun `M ignore the checkbox W map() { unique id could not be generated }`() { // Given whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockCheckedTextView, CheckableTextViewMapper.CHECKABLE_KEY_NAME ) @@ -349,7 +336,8 @@ internal abstract class BaseCheckedTextViewMapperTest : BaseWireframeMapperTest( // When val resolvedWireframes = testedCheckedTextWireframeMapper.map( mockCheckedTextView, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseNumberPickerMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseNumberPickerMapperTest.kt index 0365947624..dac39279dd 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseNumberPickerMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseNumberPickerMapperTest.kt @@ -7,12 +7,9 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.NumberPicker -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.GlobalBounds import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.FloatForgery import fr.xgouchet.elmyr.annotation.Forgery @@ -22,21 +19,11 @@ import fr.xgouchet.elmyr.annotation.StringForgery import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.mockito.Mock import org.mockito.kotlin.mock import org.mockito.kotlin.whenever internal abstract class BaseNumberPickerMapperTest : BaseWireframeMapperTest() { - @Mock - lateinit var mockViewUtils: ViewUtils - - @Mock - lateinit var mockStringUtils: StringUtils - - @Mock - lateinit var mockUniqueIdentifierGenerator: UniqueIdentifierGenerator - lateinit var testedNumberPickerMapper: BasePickerMapper @LongForgery @@ -138,52 +125,52 @@ internal abstract class BaseNumberPickerMapperTest : BaseWireframeMapperTest() { fakeExpectedNextLabelYPos = fakeExpectedBottomDividerYPos + normalizedPadding whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockNumberPicker, fakeMappingContext.systemInformation.screenDensity ) ) .thenReturn(fakeViewGlobalBounds) whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( fakeTextColor, BaseWireframeMapper.OPAQUE_ALPHA_VALUE ) ) .thenReturn(fakeExpectedSelectedLabelHtmlColor) whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( fakeTextColor, BasePickerMapper.PARTIALLY_OPAQUE_ALPHA_VALUE ) ) .thenReturn(fakeExpectedNextPrevLabelHtmlColor) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockNumberPicker, BasePickerMapper.PREV_INDEX_KEY_NAME ) ).thenReturn(fakePrevLabelId) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockNumberPicker, BasePickerMapper.DIVIDER_TOP_KEY_NAME ) ).thenReturn(fakeTopDividerId) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockNumberPicker, BasePickerMapper.SELECTED_INDEX_KEY_NAME ) ).thenReturn(fakeSelectedLabelId) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockNumberPicker, BasePickerMapper.DIVIDER_BOTTOM_KEY_NAME ) ).thenReturn(fakeBottomDividerId) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockNumberPicker, BasePickerMapper.NEXT_INDEX_KEY_NAME ) @@ -209,45 +196,57 @@ internal abstract class BaseNumberPickerMapperTest : BaseWireframeMapperTest() { fun `M return empty list W map { topDividerId null }`() { // Given whenever( - mockUniqueIdentifierGenerator + mockViewIdentifierResolver .resolveChildUniqueIdentifier( mockNumberPicker, BasePickerMapper.DIVIDER_TOP_KEY_NAME ) ) .thenReturn(null) + + // When + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) + // Then - assertThat(testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext)).isEmpty() + assertThat(wireframes).isEmpty() } @Test fun `M return empty list W map { selectedLabelId null }`() { // Given whenever( - mockUniqueIdentifierGenerator + mockViewIdentifierResolver .resolveChildUniqueIdentifier( mockNumberPicker, BasePickerMapper.SELECTED_INDEX_KEY_NAME ) ) .thenReturn(null) + + // When + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) + // Then - assertThat(testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext)).isEmpty() + assertThat(wireframes).isEmpty() } @Test fun `M return empty list W map { bottomDividerId null }`() { // Given whenever( - mockUniqueIdentifierGenerator + mockViewIdentifierResolver .resolveChildUniqueIdentifier( mockNumberPicker, BasePickerMapper.DIVIDER_BOTTOM_KEY_NAME ) ) .thenReturn(null) + + // When + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) + // Then - assertThat(testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext)).isEmpty() + assertThat(wireframes).isEmpty() } protected fun fakeNextLabelWireframe() = diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseRadioButtonMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseRadioButtonMapperTest.kt index d87c37450a..9406cfbcf4 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseRadioButtonMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseRadioButtonMapperTest.kt @@ -10,18 +10,16 @@ import android.graphics.drawable.Drawable import android.os.Build import android.widget.RadioButton import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.GlobalBounds import com.datadog.tools.unit.annotations.TestTargetApi import com.datadog.tools.unit.extensions.ApiLevelExtension import fr.xgouchet.elmyr.annotation.FloatForgery import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -33,6 +31,7 @@ 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.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -49,9 +48,6 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { lateinit var testedRadioButtonMapper: RadioButtonMapper - @Mock - lateinit var mockuniqueIdentifierGenerator: UniqueIdentifierGenerator - @Mock lateinit var mockTextWireframeMapper: TextViewMapper @@ -61,9 +57,6 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { @LongForgery var fakeGeneratedIdentifier: Long = 0L - @Mock - lateinit var mockViewUtils: ViewUtils - @Forgery lateinit var fakeViewGlobalBounds: GlobalBounds @@ -72,6 +65,9 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { @IntForgery(min = 0, max = 0xffffff) var fakeCurrentTextColor: Int = 0 + @StringForgery(regex = "#[0-9A-F]{8}") + lateinit var fakeCurrentTextColorString: String + @FloatForgery(min = 1f, max = 100f) var fakeTextSize: Float = 1f @@ -85,20 +81,25 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { whenever(it.currentTextColor).thenReturn(fakeCurrentTextColor) } whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockRadioButton, CheckableTextViewMapper.CHECKABLE_KEY_NAME ) ).thenReturn(fakeGeneratedIdentifier) + whenever(mockTextWireframeMapper.map(eq(mockRadioButton), eq(fakeMappingContext), any())) .thenReturn(fakeTextWireframes) + whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockRadioButton, fakeMappingContext.systemInformation.screenDensity ) - ) - .thenReturn(fakeViewGlobalBounds) + ).thenReturn(fakeViewGlobalBounds) + + whenever( + mockColorStringFormatter.formatColorAndAlphaAsHexString(fakeCurrentTextColor, OPAQUE_ALPHA_VALUE) + ) doReturn fakeCurrentTextColorString testedRadioButtonMapper = setupTestedMapper() } @@ -131,10 +132,6 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { } whenever(mockRadioButton.buttonDrawable).thenReturn(mockDrawable) whenever(mockRadioButton.isChecked).thenReturn(true) - val expectedRadioBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val checkBoxSize = resolveRadioBoxSize(fakeIntrinsicDrawableHeight.toLong()) val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( @@ -145,16 +142,17 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedRadioBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ), - shapeStyle = expectedCheckedShapeStyle(expectedRadioBoxColor) + shapeStyle = expectedCheckedShapeStyle(fakeCurrentTextColorString) ) // When val resolvedWireframes = testedRadioButtonMapper.map( mockRadioButton, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -170,10 +168,6 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { } whenever(mockRadioButton.buttonDrawable).thenReturn(mockDrawable) whenever(mockRadioButton.isChecked).thenReturn(false) - val expectedRadioBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val checkBoxSize = resolveRadioBoxSize(fakeIntrinsicDrawableHeight.toLong()) val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( @@ -184,16 +178,17 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedRadioBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ), - shapeStyle = expectedNotCheckedShapeStyle(expectedRadioBoxColor) + shapeStyle = expectedNotCheckedShapeStyle(fakeCurrentTextColorString) ) // When val resolvedWireframes = testedRadioButtonMapper.map( mockRadioButton, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -204,10 +199,6 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { fun `M resolve the checkbox as ShapeWireframe W map() { checked }`() { // Given whenever(mockRadioButton.isChecked).thenReturn(true) - val expectedRadioBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val checkBoxSize = resolveRadioBoxSize(CheckableCompoundButtonMapper.DEFAULT_CHECKABLE_HEIGHT_IN_PX) val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( @@ -218,16 +209,17 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedRadioBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ), - shapeStyle = expectedCheckedShapeStyle(expectedRadioBoxColor) + shapeStyle = expectedCheckedShapeStyle(fakeCurrentTextColorString) ) // When val resolvedWireframes = testedRadioButtonMapper.map( mockRadioButton, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -238,10 +230,6 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { fun `M resolve the checkbox as ShapeWireframe W map() { not checked }`() { // Given whenever(mockRadioButton.isChecked).thenReturn(false) - val expectedRadioBoxColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val checkBoxSize = resolveRadioBoxSize(CheckableCompoundButtonMapper.DEFAULT_CHECKABLE_HEIGHT_IN_PX) val expectedCheckBoxWireframe = MobileSegment.Wireframe.ShapeWireframe( @@ -252,16 +240,17 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { width = checkBoxSize, height = checkBoxSize, border = MobileSegment.ShapeBorder( - color = expectedRadioBoxColor, + color = fakeCurrentTextColorString, width = CheckableTextViewMapper.CHECKABLE_BORDER_WIDTH ), - shapeStyle = expectedNotCheckedShapeStyle(expectedRadioBoxColor) + shapeStyle = expectedNotCheckedShapeStyle(fakeCurrentTextColorString) ) // When val resolvedWireframes = testedRadioButtonMapper.map( mockRadioButton, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -272,7 +261,7 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { fun `M ignore the checkbox W map() { unique id could not be generated }`() { // Given whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockRadioButton, CheckableTextViewMapper.CHECKABLE_KEY_NAME ) @@ -281,7 +270,8 @@ internal abstract class BaseRadioButtonMapperTest : BaseWireframeMapperTest() { // When val resolvedWireframes = testedRadioButtonMapper.map( mockRadioButton, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseSeekBarWireframeMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseSeekBarWireframeMapperTest.kt index d412edf56e..5912c220b5 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseSeekBarWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseSeekBarWireframeMapperTest.kt @@ -10,12 +10,14 @@ import android.content.res.ColorStateList import android.graphics.Rect import android.graphics.drawable.Drawable import android.widget.SeekBar -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.densityNormalized -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.FloatForgery import fr.xgouchet.elmyr.annotation.Forgery @@ -127,13 +129,19 @@ internal abstract class BaseSeekBarWireframeMapperTest { lateinit var mockTrackActiveTintColors: ColorStateList @Mock - lateinit var mockViewUtils: ViewUtils + lateinit var mockViewIdentifierResolver: ViewIdentifierResolver @Mock - lateinit var mockStringUtils: StringUtils + lateinit var mockColorStringFormatter: ColorStringFormatter @Mock - lateinit var mockUniqueIdentifierGenerator: UniqueIdentifierGenerator + lateinit var mockViewBoundsResolver: ViewBoundsResolver + + @Mock + lateinit var mockDrawableToColorMapper: DrawableToColorMapper + + @Mock + lateinit var mockAsyncJobStatusCallback: AsyncJobStatusCallback lateinit var testedSeekBarWireframeMapper: SeekBarWireframeMapper @@ -179,7 +187,7 @@ internal abstract class BaseSeekBarWireframeMapperTest { fakeExpectedThumbYPos = normalizedSliderYPos + normalizedSliderTopPadding + (normalizedSliderHeight - fakeExpectedThumbHeight) / 2 whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( fakeThumbColor, SeekBarWireframeMapper.OPAQUE_ALPHA_VALUE ) @@ -187,14 +195,14 @@ internal abstract class BaseSeekBarWireframeMapperTest { .thenReturn(fakeExpectedThumbHtmlColor) whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( fakeTrackColor, SeekBarWireframeMapper.OPAQUE_ALPHA_VALUE ) ) .thenReturn(fakeExpectedTrackActiveHtmlColor) whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( fakeTrackColor, SeekBarWireframeMapper.PARTIALLY_OPAQUE_ALPHA_VALUE ) @@ -203,26 +211,26 @@ internal abstract class BaseSeekBarWireframeMapperTest { mockSeekBar = generateMockedSeekBar(forge) whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockSeekBar, fakeMappingContext.systemInformation.screenDensity ) ) .thenReturn(fakeViewGlobalBounds) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSeekBar, SeekBarWireframeMapper.TRACK_ACTIVE_KEY_NAME ) ).thenReturn(fakeActiveTrackId) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSeekBar, SeekBarWireframeMapper.TRACK_NON_ACTIVE_KEY_NAME ) ).thenReturn(fakeInactiveTrackId) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSeekBar, SeekBarWireframeMapper.THUMB_KEY_NAME ) @@ -236,42 +244,51 @@ internal abstract class BaseSeekBarWireframeMapperTest { fun `M return empty list W map { could not generate thumb id`() { // Given whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSeekBar, SeekBarWireframeMapper.THUMB_KEY_NAME ) ).thenReturn(null) + // When + val map = testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext, mockAsyncJobStatusCallback) + // Then - assertThat(testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext)).isEmpty() + assertThat(map).isEmpty() } @Test fun `M return empty list W map { could not generate active track id`() { // Given whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSeekBar, SeekBarWireframeMapper.TRACK_ACTIVE_KEY_NAME ) ).thenReturn(null) + // When + val map = testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext, mockAsyncJobStatusCallback) + // Then - assertThat(testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext)).isEmpty() + assertThat(map).isEmpty() } @Test fun `M return empty list W map { could not generate inactive track id`() { // Given whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSeekBar, SeekBarWireframeMapper.TRACK_NON_ACTIVE_KEY_NAME ) ).thenReturn(null) + // When + val map = testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext, mockAsyncJobStatusCallback) + // Then - assertThat(testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext)).isEmpty() + assertThat(map).isEmpty() } private fun generateMockedSeekBar(forge: Forge): SeekBar { diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseSwitchCompatMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseSwitchCompatMapperTest.kt index 6e1cada98f..721f29c921 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseSwitchCompatMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseSwitchCompatMapperTest.kt @@ -9,15 +9,14 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.graphics.Rect import android.graphics.drawable.Drawable import androidx.appcompat.widget.SwitchCompat -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.GlobalBounds import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -31,9 +30,6 @@ internal abstract class BaseSwitchCompatMapperTest : BaseWireframeMapperTest() { lateinit var testedSwitchCompatMapper: SwitchCompatMapper - @Mock - lateinit var mockuniqueIdentifierGenerator: UniqueIdentifierGenerator - @Mock lateinit var mockTextWireframeMapper: TextViewMapper @@ -45,9 +41,6 @@ internal abstract class BaseSwitchCompatMapperTest : BaseWireframeMapperTest() { @LongForgery var fakeTrackIdentifier: Long = 0L - @Mock - lateinit var mockViewUtils: ViewUtils - @Forgery lateinit var fakeViewGlobalBounds: GlobalBounds @@ -80,6 +73,9 @@ internal abstract class BaseSwitchCompatMapperTest : BaseWireframeMapperTest() { @IntForgery(min = 0, max = 0xffffff) var fakeCurrentTextColor: Int = 0 + @StringForgery(regex = "#[0-9A-F]{8}") + lateinit var fakeCurrentTextColorString: String + private var normalizedThumbHeight: Long = 0 protected var normalizedThumbWidth: Long = 0 private var normalizedTrackWidth: Long = 0 @@ -118,13 +114,13 @@ internal abstract class BaseSwitchCompatMapperTest : BaseWireframeMapperTest() { whenever(it.thumbDrawable).thenReturn(mockThumbDrawable) } whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSwitch, SwitchCompatMapper.TRACK_KEY_NAME ) ).thenReturn(fakeTrackIdentifier) whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSwitch, SwitchCompatMapper.THUMB_KEY_NAME ) @@ -132,11 +128,16 @@ internal abstract class BaseSwitchCompatMapperTest : BaseWireframeMapperTest() { whenever(mockTextWireframeMapper.map(eq(mockSwitch), eq(fakeMappingContext), any())) .thenReturn(fakeTextWireframes) whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockSwitch, fakeMappingContext.systemInformation.screenDensity ) ).thenReturn(fakeViewGlobalBounds) + + whenever( + mockColorStringFormatter.formatColorAndAlphaAsHexString(fakeCurrentTextColor, OPAQUE_ALPHA_VALUE) + ).thenReturn(fakeCurrentTextColorString) + testedSwitchCompatMapper = setupTestedMapper() } @@ -151,7 +152,8 @@ internal abstract class BaseSwitchCompatMapperTest : BaseWireframeMapperTest() { // When val resolvedWireframes = testedSwitchCompatMapper.map( mockSwitch, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -167,7 +169,8 @@ internal abstract class BaseSwitchCompatMapperTest : BaseWireframeMapperTest() { // When val resolvedWireframes = testedSwitchCompatMapper.map( mockSwitch, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -180,7 +183,7 @@ internal abstract class BaseSwitchCompatMapperTest : BaseWireframeMapperTest() { ) { // Given whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSwitch, SwitchCompatMapper.TRACK_KEY_NAME ) @@ -190,7 +193,8 @@ internal abstract class BaseSwitchCompatMapperTest : BaseWireframeMapperTest() { // When val resolvedWireframes = testedSwitchCompatMapper.map( mockSwitch, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseTextViewWireframeMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseTextViewWireframeMapperTest.kt index 8e7d0ecbd9..2785730430 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseTextViewWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseTextViewWireframeMapperTest.kt @@ -13,17 +13,18 @@ import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.util.DisplayMetrics import android.widget.TextView -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.aMockTextView import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.internal.recorder.obfuscator.rules.TextValueObfuscationRule -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelperCallback import com.datadog.android.sessionreplay.internal.utils.shapeStyle import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.ImageWireframeHelper import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.FloatForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery import fr.xgouchet.elmyr.annotation.StringForgery import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach @@ -33,10 +34,10 @@ import org.junit.jupiter.params.provider.MethodSource import org.mockito.Mock import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTest() { @@ -52,9 +53,6 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes @Mock lateinit var mockImageWireframeHelper: ImageWireframeHelper - @Mock - lateinit var mockUniqueIdentifierGenerator: UniqueIdentifierGenerator - @Mock lateinit var mockDisplayMetrics: DisplayMetrics @@ -64,23 +62,68 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes @StringForgery lateinit var fakeDefaultObfuscatedText: String - @Mock - lateinit var mockJobStatusCallback: AsyncJobStatusCallback + @LongForgery + var fakeWireframeId: Long = 0 + + @IntForgery(min = 0, max = 0xffffff) + var fakeCurrentTextColor: Int = 0 + + @StringForgery(regex = "#[0-9A-F]{8}") + lateinit var fakeCurrentTextColorString: String + + @Forgery + lateinit var fakeBounds: GlobalBounds @BeforeEach fun `set up`() { - whenever(mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier(any(), any())) + fakeMappingContext = fakeMappingContext.copy(imageWireframeHelper = mockImageWireframeHelper) + whenever(mockViewIdentifierResolver.resolveChildUniqueIdentifier(any(), any())) .thenReturn(System.identityHashCode(this).toLong()) whenever(mockResources.displayMetrics).thenReturn(mockDisplayMetrics) + + whenever(mockViewIdentifierResolver.resolveViewId(any())) doReturn fakeWireframeId + + whenever(mockViewBoundsResolver.resolveViewGlobalBounds(any(), any())) + .thenReturn(fakeBounds) + + whenever( + mockColorStringFormatter.formatColorAndAlphaAsHexString( + fakeCurrentTextColor, + OPAQUE_ALPHA_VALUE + ) + ) doReturn fakeCurrentTextColorString + testedTextWireframeMapper = initTestedMapper() } - protected open fun initTestedMapper(): TextViewMapper { - return TextViewMapper( - imageWireframeHelper = mockImageWireframeHelper, - uniqueIdentifierGenerator = mockUniqueIdentifierGenerator, - textValueObfuscationRule = mockObfuscationRule + abstract fun initTestedMapper(): TextViewMapper + + protected fun TextView.toTextWireframes(): List { + val screenDensity = fakeMappingContext.systemInformation.screenDensity + return listOf( + MobileSegment.Wireframe.TextWireframe( + id = fakeWireframeId, + x = fakeBounds.x, + y = fakeBounds.y, + width = fakeBounds.width, + height = fakeBounds.height, + text = resolveTextValue(this), + textStyle = MobileSegment.TextStyle( + TextViewMapper.SANS_SERIF_FAMILY_NAME, + textSize.toLong().densityNormalized(screenDensity), + fakeCurrentTextColorString + ), + textPosition = MobileSegment.TextPosition( + MobileSegment.Padding(0, 0, 0, 0), + alignment = + MobileSegment.Alignment( + MobileSegment.Horizontal.LEFT, + MobileSegment.Vertical + .CENTER + ) + ) + ) ) } @@ -93,16 +136,10 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes ) { // Given val fakeFontSize = forge.aFloat(min = 0f) - val fakeStyleColor = forge.aStringMatching("#[0-9a-f]{6}ff") - val fakeFontColor = fakeStyleColor - .substring(1) - .toLong(16) - .shr(8) - .toInt() val mockTextView: TextView = forge.aMockTextView().apply { whenever(this.typeface).thenReturn(fakeTypeface) whenever(this.textSize).thenReturn(fakeFontSize) - whenever(this.currentTextColor).thenReturn(fakeFontColor) + whenever(this.currentTextColor).thenReturn(fakeCurrentTextColor) whenever(this.text).thenReturn(fakeText) whenever(this.resources).thenReturn(mockResources) } @@ -111,7 +148,7 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes .thenReturn(fakeDefaultObfuscatedText) // When - val textWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext) + val textWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext, mockAsyncJobStatusCallback) // Then val expectedWireframes = mockTextView.toTextWireframes().map { @@ -120,7 +157,7 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes textStyle = MobileSegment.TextStyle( expectedFontFamily, fakeFontSize.toLong().densityNormalized(fakeMappingContext.systemInformation.screenDensity), - fakeStyleColor + fakeCurrentTextColorString ) ) } @@ -140,12 +177,13 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes whenever(this.typeface).thenReturn(mock()) whenever(this.textAlignment).thenReturn(fakeTextAlignment) whenever(this.resources).thenReturn(mockResources) + whenever(this.currentTextColor).thenReturn(fakeCurrentTextColor) } whenever(mockObfuscationRule.resolveObfuscatedValue(mockTextView, fakeMappingContext)) .thenReturn(fakeDefaultObfuscatedText) // When - val textWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext) + val textWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext, mockAsyncJobStatusCallback) // Then val expectedWireframes = mockTextView.toTextWireframes().map { @@ -174,13 +212,13 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes whenever(this.textAlignment).thenReturn(TextView.TEXT_ALIGNMENT_GRAVITY) whenever(this.gravity).thenReturn(fakeGravity) whenever(this.resources).thenReturn(mockResources) + whenever(this.currentTextColor).thenReturn(fakeCurrentTextColor) } - whenever(mockObfuscationRule.resolveObfuscatedValue(mockTextView, fakeMappingContext)) .thenReturn(fakeDefaultObfuscatedText) // When - val textWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext) + val textWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext, mockAsyncJobStatusCallback) // Then val expectedWireframes = mockTextView.toTextWireframes().map { @@ -196,7 +234,9 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes } @Test - fun `M resolve a TextWireframe W map() { TextView with textPadding }`(forge: Forge) { + fun `M resolve a TextWireframe W map() { TextView with textPadding }`( + forge: Forge + ) { // Given val fakeTextPaddingTop = forge.anInt() val fakeTextPaddingBottom = forge.anInt() @@ -210,6 +250,7 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes whenever(this.totalPaddingStart).thenReturn(fakeTextPaddingStart) whenever(this.totalPaddingEnd).thenReturn(fakeTextPaddingEnd) whenever(this.resources).thenReturn(mockResources) + whenever(this.currentTextColor).thenReturn(fakeCurrentTextColor) } val expectedWireframeTextPadding = MobileSegment.Padding( fakeTextPaddingTop.densityNormalized(fakeMappingContext.systemInformation.screenDensity).toLong(), @@ -221,7 +262,7 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes .thenReturn(fakeDefaultObfuscatedText) // When - val textWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext) + val textWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext, mockAsyncJobStatusCallback) // Then val expectedWireframes = mockTextView.toTextWireframes().map { @@ -242,43 +283,39 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes @Test fun `M resolve a ShapeWireframe background W map {ColorDrawable background}`( - forge: Forge + forge: Forge, + @IntForgery fakeBackgroundColor: Int, + @StringForgery(regex = "#[0-9A-F]{8}") fakeBackgroundColorString: String, + @FloatForgery(0f, 1f) fakeViewAlpha: Float ) { // Given - val fakeStyleColor = forge.aStringMatching("#[0-9a-f]{8}") - val fakeViewAlpha = forge.aFloat(min = 0f, max = 1f) - val fakeDrawableColor = fakeStyleColor - .substring(1) - .toLong(16) - .shr(8) - .toInt() - val fakeDrawableAlpha = fakeStyleColor - .substring(1) - .toLong(16) - .and(ALPHA_MASK) - .toInt() - val mockDrawable = mock { - whenever(it.color).thenReturn(fakeDrawableColor) - whenever(it.alpha).thenReturn(fakeDrawableAlpha) - } + val mockDrawable = mock() val mockTextView = forge.aMockTextView().apply { whenever(this.background).thenReturn(mockDrawable) whenever(this.text).thenReturn(fakeText) whenever(this.typeface).thenReturn(mock()) whenever(this.alpha).thenReturn(fakeViewAlpha) whenever(this.resources).thenReturn(mockResources) + whenever(this.currentTextColor).thenReturn(fakeCurrentTextColor) } whenever(mockObfuscationRule.resolveObfuscatedValue(mockTextView, fakeMappingContext)) .thenReturn(fakeDefaultObfuscatedText) + whenever(mockDrawableToColorMapper.mapDrawableToColor(mockDrawable)).thenReturn(fakeBackgroundColor) + whenever(mockColorStringFormatter.formatColorAsHexString(fakeBackgroundColor)) + .thenReturn(fakeBackgroundColorString) // When - val actualWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext) + val actualWireframes = testedTextWireframeMapper.map( + mockTextView, + fakeMappingContext, + mockAsyncJobStatusCallback + ) // Then val textWireframes = mockTextView.toTextWireframes() val backgroundWireframe = MobileSegment.Wireframe.ShapeWireframe( - id = System.identityHashCode(this).toLong(), + id = fakeWireframeId, x = textWireframes[0].x, y = textWireframes[0].y, width = mockTextView.width.densityNormalized( @@ -288,7 +325,7 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes fakeMappingContext.systemInformation.screenDensity ).toLong(), shapeStyle = MobileSegment.ShapeStyle( - backgroundColor = fakeStyleColor, + backgroundColor = fakeBackgroundColorString, opacity = fakeViewAlpha, cornerRadius = null ), @@ -319,19 +356,20 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes ) { // Given val fakeViewAlpha = forge.aFloat(min = 0f, max = 1f) - val mockDrawable = mock() - val mockDrawableCopy = mock() - val mockConstantState = mock() { - whenever(it.newDrawable(any())).thenReturn(mockDrawableCopy) + val mockDrawable = mock().also { mockDrawable -> + val mockConstantState = mock().also { mockState -> + whenever(mockState.newDrawable(anyOrNull())) doReturn mockDrawable + } + whenever(mockDrawable.constantState) doReturn mockConstantState } - whenever(mockDrawable.constantState).thenReturn(mockConstantState) - + whenever(mockDrawableToColorMapper.mapDrawableToColor(mockDrawable)) doReturn null val mockTextView = forge.aMockTextView().apply { whenever(this.background).thenReturn(mockDrawable) whenever(this.text).thenReturn(fakeText) whenever(this.typeface).thenReturn(mock()) whenever(this.alpha).thenReturn(fakeViewAlpha) whenever(this.resources).thenReturn(mockResources) + whenever(this.currentTextColor).thenReturn(fakeCurrentTextColor) } val fakeBackgroundWireframe: MobileSegment.Wireframe.ImageWireframe = forge.getForgery() whenever(mockObfuscationRule.resolveObfuscatedValue(mockTextView, fakeMappingContext)) @@ -345,12 +383,12 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes width = any(), height = any(), usePIIPlaceholder = any(), - drawable = anyOrNull(), + drawable = any(), shapeStyle = anyOrNull(), border = anyOrNull(), clipping = anyOrNull(), prefix = anyOrNull(), - imageWireframeHelperCallback = anyOrNull() + asyncJobStatusCallback = anyOrNull() ) ).thenReturn(fakeBackgroundWireframe) @@ -358,7 +396,7 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes val actualWireframes = testedTextWireframeMapper.map( mockTextView, fakeMappingContext, - mockJobStatusCallback + mockAsyncJobStatusCallback ) // Then @@ -377,38 +415,33 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes assertThat(actualWireframes[0]).isEqualTo(fakeBackgroundWireframe) assertThat((actualWireframes[1] as MobileSegment.Wireframe.TextWireframe).text) .isEqualTo((expectedWireframes[1] as MobileSegment.Wireframe.TextWireframe).text) - argumentCaptor() { - verify(mockImageWireframeHelper).createImageWireframe( - view = any(), - currentWireframeIndex = any(), - x = any(), - y = any(), - width = any(), - height = any(), - usePIIPlaceholder = any(), - drawable = anyOrNull(), - shapeStyle = anyOrNull(), - border = anyOrNull(), - clipping = anyOrNull(), - imageWireframeHelperCallback = capture(), - prefix = anyOrNull() - ) - allValues.forEach() { - it.onStart() - it.onFinished() - } - verify(mockJobStatusCallback).jobStarted() - verify(mockJobStatusCallback).jobFinished() - verifyNoMoreInteractions(mockJobStatusCallback) - } + + verify(mockImageWireframeHelper).createImageWireframe( + view = eq(mockTextView), + currentWireframeIndex = any(), + x = any(), + y = any(), + width = any(), + height = any(), + usePIIPlaceholder = any(), + drawable = any(), + asyncJobStatusCallback = eq(mockAsyncJobStatusCallback), + clipping = anyOrNull(), + shapeStyle = anyOrNull(), + border = anyOrNull(), + prefix = anyOrNull() + ) } @Test - fun `M resolve a TextWireframe W map() { TextView without text, with hint }`(forge: Forge) { + fun `M resolve a TextWireframe W map() { TextView without text, with hint }`( + forge: Forge, + @StringForgery fakeHintText: String, + @IntForgery(0, 0xFFFFFFF) fakeHintColor: Int, + @StringForgery(regex = "#[0-9A-F]{8}") fakeHintColorString: String + ) { // Given val fakeDefaultObfuscatedText = forge.aString() - val fakeHintText = forge.aString() - val fakeHintColor = forge.anInt(min = 0, max = 0xffffff) val mockColorStateList: ColorStateList = mock { whenever(it.defaultColor).thenReturn(fakeHintColor) } @@ -418,12 +451,15 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes whenever(this.hintTextColors).thenReturn(mockColorStateList) whenever(this.typeface).thenReturn(mock()) whenever(this.resources).thenReturn(mockResources) + whenever(this.currentTextColor).thenReturn(fakeCurrentTextColor) } whenever(mockObfuscationRule.resolveObfuscatedValue(mockTextView, fakeMappingContext)) .thenReturn(fakeDefaultObfuscatedText) + whenever(mockColorStringFormatter.formatColorAndAlphaAsHexString(fakeHintColor, 255)) + .doReturn(fakeHintColorString) // When - val textWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext) + val textWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext, mockAsyncJobStatusCallback) // Then val expectedWireframes = mockTextView @@ -435,10 +471,7 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes TextViewMapper.SANS_SERIF_FAMILY_NAME, mockTextView.textSize.toLong() .densityNormalized(fakeMappingContext.systemInformation.screenDensity), - StringUtils.formatColorAndAlphaAsHexa( - fakeHintColor, - OPAQUE_ALPHA_VALUE - ) + fakeHintColorString ) ) } @@ -452,13 +485,12 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes // Given val fakeDefaultObfuscatedText = forge.aString() val fakeHintText = forge.aString() - val fakeTextColor = forge.anInt(min = 0, max = 0xffffff) val mockTextView: TextView = forge.aMockTextView().apply { whenever(this.text).thenReturn("") whenever(this.hint).thenReturn(fakeHintText) whenever(this.hintTextColors).thenReturn(null) whenever(this.typeface).thenReturn(mock()) - whenever(this.currentTextColor).thenReturn(fakeTextColor) + whenever(this.currentTextColor).thenReturn(fakeCurrentTextColor) whenever(this.resources).thenReturn(mockResources) } @@ -466,7 +498,7 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes .thenReturn(fakeDefaultObfuscatedText) // When - val textWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext) + val textWireframes = testedTextWireframeMapper.map(mockTextView, fakeMappingContext, mockAsyncJobStatusCallback) // Then val expectedWireframes = mockTextView @@ -478,10 +510,7 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes TextViewMapper.SANS_SERIF_FAMILY_NAME, mockTextView.textSize.toLong() .densityNormalized(fakeMappingContext.systemInformation.screenDensity), - StringUtils.formatColorAndAlphaAsHexa( - fakeTextColor, - OPAQUE_ALPHA_VALUE - ) + fakeCurrentTextColorString ) ) } @@ -493,7 +522,6 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes // Given val fakeDefaultObfuscatedText = forge.aString() val fakeHintText = forge.aString() - val fakeTextColor = forge.anInt(min = 0, max = 0xffffff) val fakeDrawables = arrayOf( mock(), mock(), @@ -505,7 +533,7 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes whenever(this.hint).thenReturn(fakeHintText) whenever(this.hintTextColors).thenReturn(null) whenever(this.typeface).thenReturn(mock()) - whenever(this.currentTextColor).thenReturn(fakeTextColor) + whenever(this.currentTextColor).thenReturn(fakeCurrentTextColor) whenever(this.resources).thenReturn(mockResources) whenever(this.compoundDrawables).thenReturn( fakeDrawables @@ -528,27 +556,18 @@ internal abstract class BaseTextViewWireframeMapperTest : BaseWireframeMapperTes val wireframes = testedTextWireframeMapper.map( mockTextView, fakeMappingContext, - mockJobStatusCallback + mockAsyncJobStatusCallback ) - val imageWireframes = wireframes.filter { it is MobileSegment.Wireframe.ImageWireframe } + val imageWireframes = wireframes.filterIsInstance() // Then - argumentCaptor() { - verify(mockImageWireframeHelper) - .createCompoundDrawableWireframes( - any(), - any(), - any(), - capture() - ) - allValues.forEach { - it.onStart() - it.onFinished() - } - verify(mockJobStatusCallback).jobStarted() - verify(mockJobStatusCallback).jobFinished() - verifyNoMoreInteractions(mockJobStatusCallback) - } + verify(mockImageWireframeHelper) + .createCompoundDrawableWireframes( + any(), + any(), + any(), + eq(mockAsyncJobStatusCallback) + ) assertThat(imageWireframes).isEqualTo(listOf(mockImageWireframe)) } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapperTest.kt index f616319bad..9071b953b3 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapperTest.kt @@ -12,13 +12,17 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import com.datadog.android.sessionreplay.internal.recorder.MappingContext -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import com.datadog.tools.unit.setStaticValue import fr.xgouchet.elmyr.annotation.Forgery import org.junit.jupiter.api.AfterAll import org.junit.jupiter.params.provider.Arguments +import org.mockito.Mock import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import java.util.stream.Stream @@ -28,6 +32,21 @@ internal abstract class BaseWireframeMapperTest { @Forgery lateinit var fakeMappingContext: MappingContext + @Mock + lateinit var mockViewIdentifierResolver: ViewIdentifierResolver + + @Mock + lateinit var mockColorStringFormatter: ColorStringFormatter + + @Mock + lateinit var mockViewBoundsResolver: ViewBoundsResolver + + @Mock + lateinit var mockDrawableToColorMapper: DrawableToColorMapper + + @Mock + lateinit var mockAsyncJobStatusCallback: AsyncJobStatusCallback + protected fun mockNonDecorView(): View { return mock { whenever(it.parent).thenReturn(mock()) @@ -44,56 +63,6 @@ internal abstract class BaseWireframeMapperTest { } } - protected fun View.toShapeWireframes(): List { - val coordinates = IntArray(2) - val screenDensity = fakeMappingContext.systemInformation.screenDensity - this.getLocationOnScreen(coordinates) - val x = coordinates[0].densityNormalized(screenDensity).toLong() - val y = coordinates[1].densityNormalized(screenDensity).toLong() - return listOf( - MobileSegment.Wireframe.ShapeWireframe( - System.identityHashCode(this).toLong(), - x = x, - y = y, - width = width.toLong().densityNormalized(screenDensity), - height = height.toLong().densityNormalized(screenDensity) - ) - ) - } - - protected fun TextView.toTextWireframes(): List { - val coordinates = IntArray(2) - this.getLocationOnScreen(coordinates) - val screenDensity = fakeMappingContext.systemInformation.screenDensity - val x = coordinates[0].densityNormalized(screenDensity).toLong() - val y = coordinates[1].densityNormalized(screenDensity).toLong() - val textColor = StringUtils.formatColorAndAlphaAsHexa(currentTextColor, OPAQUE_ALPHA_VALUE) - return listOf( - MobileSegment.Wireframe.TextWireframe( - System.identityHashCode(this).toLong(), - x = x, - y = y, - text = resolveTextValue(this), - width = width.toLong().densityNormalized(screenDensity), - height = height.toLong().densityNormalized(screenDensity), - textStyle = MobileSegment.TextStyle( - TextViewMapper.SANS_SERIF_FAMILY_NAME, - textSize.toLong().densityNormalized(screenDensity), - textColor - ), - textPosition = MobileSegment.TextPosition( - MobileSegment.Padding(0, 0, 0, 0), - alignment = - MobileSegment.Alignment( - MobileSegment.Horizontal.LEFT, - MobileSegment.Vertical - .CENTER - ) - ) - ) - ) - } - protected open fun resolveTextValue(textView: TextView): String { return textView.text.toString() } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ButtonMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ButtonMapperTest.kt index bb89c50998..5dd629a15d 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ButtonMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ButtonMapperTest.kt @@ -72,7 +72,7 @@ internal class ButtonMapperTest : BaseWireframeMapperTest() { ).thenReturn(fakeTextWireframes) // When - val buttonWireframes = testedButtonMapper.map(mockButton, fakeMappingContext) + val buttonWireframes = testedButtonMapper.map(mockButton, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(buttonWireframes).isEqualTo(fakeTextWireframes) @@ -93,7 +93,7 @@ internal class ButtonMapperTest : BaseWireframeMapperTest() { ).thenReturn(fakeTextWireframes) // When - val buttonWireframes = testedButtonMapper.map(mockButton, fakeMappingContext) + val buttonWireframes = testedButtonMapper.map(mockButton, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(buttonWireframes).isEqualTo(fakeTextWireframes) @@ -116,7 +116,7 @@ internal class ButtonMapperTest : BaseWireframeMapperTest() { } // When - val buttonWireframes = testedButtonMapper.map(mockButton, fakeMappingContext) + val buttonWireframes = testedButtonMapper.map(mockButton, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(buttonWireframes).isEqualTo(expectedWireframes) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckBoxMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckBoxMapperTest.kt index 9b0946f5a1..241e442780 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckBoxMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckBoxMapperTest.kt @@ -27,9 +27,11 @@ internal class CheckBoxMapperTest : BaseCheckBoxMapperTest() { override fun setupTestedMapper(): CheckBoxMapper { return CheckBoxMapper( - textWireframeMapper = mockTextWireframeMapper, - uniqueIdentifierGenerator = mockuniqueIdentifierGenerator, - viewUtils = mockViewUtils + mockTextWireframeMapper, + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper ) } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckedTextViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckedTextViewMapperTest.kt index 174f57187a..3eeb4e0672 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckedTextViewMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckedTextViewMapperTest.kt @@ -27,9 +27,11 @@ internal class CheckedTextViewMapperTest : BaseCheckedTextViewMapperTest() { override fun setupTestedMapper(): CheckedTextViewMapper { return CheckedTextViewMapper( - textWireframeMapper = mockTextWireframeMapper, - uniqueIdentifierGenerator = mockuniqueIdentifierGenerator, - viewUtils = mockViewUtils + mockTextWireframeMapper, + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper ) } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/DecorViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/DecorViewMapperTest.kt index 9250dd6079..834f8553ce 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/DecorViewMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/DecorViewMapperTest.kt @@ -11,7 +11,6 @@ import android.view.View import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.sessionreplay.internal.recorder.aMockView import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.LongForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration @@ -40,9 +39,6 @@ internal class DecorViewMapperTest : BaseWireframeMapperTest() { @Mock lateinit var mockViewWireframeMapper: ViewWireframeMapper - @Mock - lateinit var mockuniqueIdentifierGenerator: UniqueIdentifierGenerator - lateinit var mockDecorView: View lateinit var mockViewWireframes: List @@ -56,7 +52,7 @@ internal class DecorViewMapperTest : BaseWireframeMapperTest() { fun `set up`(forge: Forge) { mockDecorView = forge.aMockView() whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockDecorView, DecorViewMapper.WINDOW_KEY_NAME ) @@ -69,7 +65,7 @@ internal class DecorViewMapperTest : BaseWireframeMapperTest() { .thenReturn(mockViewWireframes) testedDecorViewMapper = DecorViewMapper( mockViewWireframeMapper, - mockuniqueIdentifierGenerator + mockViewIdentifierResolver ) } @@ -101,7 +97,7 @@ internal class DecorViewMapperTest : BaseWireframeMapperTest() { // When val mappedDecorViewWireframes = testedDecorViewMapper - .map(mockDecorView, fakeMappingContext) + .map(mockDecorView, fakeMappingContext, mockAsyncJobStatusCallback) .filter { it.id in viewWireframesIds } // Then @@ -133,7 +129,7 @@ internal class DecorViewMapperTest : BaseWireframeMapperTest() { // When val mappedDecorViewWireframes = testedDecorViewMapper - .map(mockDecorView, fakeMappingContext) + .map(mockDecorView, fakeMappingContext, mockAsyncJobStatusCallback) .filter { it.id in viewWireframesIds } // Then @@ -157,7 +153,7 @@ internal class DecorViewMapperTest : BaseWireframeMapperTest() { // When val mappedDecorViewWireframes = testedDecorViewMapper - .map(mockDecorView, fakeMappingContext) + .map(mockDecorView, fakeMappingContext, mockAsyncJobStatusCallback) .filter { it.id in viewWireframesIds } // Then @@ -184,7 +180,7 @@ internal class DecorViewMapperTest : BaseWireframeMapperTest() { ) // When - val wireframes = testedDecorViewMapper.map(mockDecorView, fakeMappingContext) + val wireframes = testedDecorViewMapper.map(mockDecorView, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes.size).isEqualTo(mockViewWireframes.size + 1) @@ -195,7 +191,7 @@ internal class DecorViewMapperTest : BaseWireframeMapperTest() { fun `M do not handle the Window background W map {uniqueIdentifier could not be generated}`() { // Given whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockDecorView, DecorViewMapper.WINDOW_KEY_NAME ) @@ -213,7 +209,7 @@ internal class DecorViewMapperTest : BaseWireframeMapperTest() { ) // When - val wireframes = testedDecorViewMapper.map(mockDecorView, fakeMappingContext) + val wireframes = testedDecorViewMapper.map(mockDecorView, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes.size).isEqualTo(mockViewWireframes.size) @@ -225,7 +221,7 @@ internal class DecorViewMapperTest : BaseWireframeMapperTest() { // Given val mockPopUpDecorView = forge.aMockView() whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockPopUpDecorView, DecorViewMapper.WINDOW_KEY_NAME ) @@ -245,7 +241,7 @@ internal class DecorViewMapperTest : BaseWireframeMapperTest() { ) // When - val wireframes = testedDecorViewMapper.map(mockPopUpDecorView, fakeMappingContext) + val wireframes = testedDecorViewMapper.map(mockPopUpDecorView, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes.size).isEqualTo(mockViewWireframes.size) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapperTest.kt index bdb5684393..8896a5fbe6 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapperTest.kt @@ -13,21 +13,25 @@ import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable.ConstantState import android.util.DisplayMetrics +import android.view.View import android.widget.ImageView import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.SystemInformation import com.datadog.android.sessionreplay.internal.recorder.resources.ImageCompression -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelperCallback import com.datadog.android.sessionreplay.internal.utils.ImageViewUtils import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.ImageWireframeHelper +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -40,10 +44,9 @@ import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.times +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -74,9 +77,6 @@ internal class ImageViewMapperTest { @Mock lateinit var mockDrawable: Drawable - @Mock - lateinit var mockUniqueIdentifierGenerator: UniqueIdentifierGenerator - @Mock lateinit var mockSystemInformation: SystemInformation @@ -87,20 +87,32 @@ internal class ImageViewMapperTest { lateinit var mockDisplayMetrics: DisplayMetrics @Mock - lateinit var mockCallback: AsyncJobStatusCallback + lateinit var mockAsyncJobStatusCallback: AsyncJobStatusCallback @Mock - lateinit var mockViewUtils: ViewUtils + lateinit var mockViewIdentifierResolver: ViewIdentifierResolver + + @Mock + lateinit var mockColorStringFormatter: ColorStringFormatter + + @Mock + lateinit var mockViewBoundsResolver: ViewBoundsResolver + + @Mock + lateinit var mockDrawableToColorMapper: DrawableToColorMapper @Mock lateinit var mockGlobalBounds: GlobalBounds @Mock - lateinit var mockBackground: Drawable + lateinit var mockBackgroundDrawable: Drawable @Mock lateinit var mockConstantState: ConstantState + @Mock + lateinit var mockBackgroundConstantState: ConstantState + @Mock lateinit var stubClipping: MobileSegment.WireframeClip @@ -113,17 +125,19 @@ internal class ImageViewMapperTest { @Mock lateinit var mockContext: Context - private val fakeId = Forge().aLong() + @LongForgery + var fakeId: Long = 0L - private val fakeMimeType = Forge().aString() + @StringForgery(regex = "\\w+/\\w+") + lateinit var fakeMimeType: String - private lateinit var expectedWireframe: MobileSegment.Wireframe.ImageWireframe + private lateinit var expectedImageWireframe: MobileSegment.Wireframe.ImageWireframe @BeforeEach fun setup(forge: Forge) { whenever(mockImageView.background).thenReturn(null) - whenever(mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier(any(), any())) + whenever(mockViewIdentifierResolver.resolveChildUniqueIdentifier(any(), any())) .thenReturn(fakeId) whenever(mockConstantState.newDrawable(any())).thenReturn(mockDrawable) @@ -134,10 +148,9 @@ internal class ImageViewMapperTest { whenever(mockDrawable.intrinsicWidth).thenReturn(forge.aPositiveInt()) whenever(mockDrawable.intrinsicHeight).thenReturn(forge.aPositiveInt()) - whenever(mockWebPImageCompression.getMimeType()).thenReturn(fakeMimeType) - whenever(mockSystemInformation.screenDensity).thenReturn(forge.aFloat()) whenever(mockMappingContext.systemInformation).thenReturn(mockSystemInformation) + whenever(mockMappingContext.imageWireframeHelper).thenReturn(mockImageWireframeHelper) whenever(mockResources.displayMetrics).thenReturn(mockDisplayMetrics) whenever(mockImageView.resources).thenReturn(mockResources) @@ -145,7 +158,8 @@ internal class ImageViewMapperTest { whenever(mockContext.applicationContext).thenReturn(mockContext) whenever(mockImageView.context).thenReturn(mockContext) - whenever(mockBackground.current).thenReturn(mockBackground) + whenever(mockBackgroundDrawable.current).thenReturn(mockBackgroundDrawable) + whenever(mockDrawableToColorMapper.mapDrawableToColor(any())) doReturn null whenever(stubImageViewUtils.resolveParentRectAbsPosition(any())).thenReturn(stubParentRect) whenever(stubImageViewUtils.resolveContentRectWithScaling(any(), any())).thenReturn(stubContentRect) @@ -155,9 +169,12 @@ internal class ImageViewMapperTest { whenever(stubContentRect.width()).thenReturn(forge.aPositiveInt()) whenever(stubContentRect.height()).thenReturn(forge.aPositiveInt()) - whenever(mockViewUtils.resolveViewGlobalBounds(any(), any())).thenReturn(mockGlobalBounds) + whenever(mockViewBoundsResolver.resolveViewGlobalBounds(any(), any())).thenReturn(mockGlobalBounds) - expectedWireframe = MobileSegment.Wireframe.ImageWireframe( + whenever(mockBackgroundConstantState.newDrawable(any())) doReturn mockBackgroundDrawable + whenever(mockBackgroundDrawable.constantState) doReturn mockBackgroundConstantState + + expectedImageWireframe = MobileSegment.Wireframe.ImageWireframe( id = fakeId, x = mockGlobalBounds.x, y = mockGlobalBounds.y, @@ -170,9 +187,11 @@ internal class ImageViewMapperTest { ) testedMapper = ImageViewMapper( - imageWireframeHelper = mockImageWireframeHelper, - uniqueIdentifierGenerator = mockUniqueIdentifierGenerator, - imageViewUtils = stubImageViewUtils + stubImageViewUtils, + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper ) } @@ -180,14 +199,21 @@ internal class ImageViewMapperTest { fun `M return foreground wireframe W map() { no background }`() { // Given whenever(mockImageView.background).thenReturn(null) - mockCreateImageWireframe(null, expectedWireframe) + mockImageWireframeHelper( + expectedView = mockImageView, + expectedDrawable = mockDrawable, + expectedIndex = 0, + expectedPrefix = ImageWireframeHelper.DRAWABLE_CHILD_NAME, + expectedUsePIIPlaceholder = true, + returnedWireframe = expectedImageWireframe + ) // When - val wireframes = testedMapper.map(mockImageView, mockMappingContext) + val wireframes = testedMapper.map(mockImageView, mockMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes.size).isEqualTo(1) - assertThat(wireframes[0]).isEqualTo(expectedWireframe) + assertThat(wireframes[0]).isEqualTo(expectedImageWireframe) } @Test @@ -206,17 +232,32 @@ internal class ImageViewMapperTest { mimeType = fakeMimeType, isEmpty = true ) - - whenever(mockImageView.background).thenReturn(mockBackground) - mockCreateImageWireframe(expectedBackgroundWireframe, expectedWireframe) + whenever(mockDrawableToColorMapper.mapDrawableToColor(mockBackgroundDrawable)) doReturn null + whenever(mockImageView.background).thenReturn(mockBackgroundDrawable) + mockImageWireframeHelper( + expectedView = mockImageView, + expectedDrawable = mockBackgroundDrawable, + expectedIndex = 0, + expectedPrefix = BaseAsyncBackgroundWireframeMapper.PREFIX_BACKGROUND_DRAWABLE, + expectedUsePIIPlaceholder = false, + returnedWireframe = expectedBackgroundWireframe + ) + mockImageWireframeHelper( + expectedView = mockImageView, + expectedDrawable = mockDrawable, + expectedIndex = 1, + expectedPrefix = ImageWireframeHelper.DRAWABLE_CHILD_NAME, + expectedUsePIIPlaceholder = true, + returnedWireframe = expectedImageWireframe + ) // When - val wireframes = testedMapper.map(mockImageView, mockMappingContext) + val wireframes = testedMapper.map(mockImageView, mockMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes.size).isEqualTo(2) assertThat(wireframes[0]).isEqualTo(expectedBackgroundWireframe) - assertThat(wireframes[1]).isEqualTo(expectedWireframe) + assertThat(wireframes[1]).isEqualTo(expectedImageWireframe) } @Test @@ -231,24 +272,23 @@ internal class ImageViewMapperTest { width = any(), height = any(), usePIIPlaceholder = any(), - drawable = anyOrNull(), + drawable = any(), + asyncJobStatusCallback = anyOrNull(), + clipping = anyOrNull(), shapeStyle = anyOrNull(), border = anyOrNull(), - clipping = anyOrNull(), - prefix = anyOrNull(), - imageWireframeHelperCallback = anyOrNull() + prefix = anyOrNull() ) - ).thenReturn(expectedWireframe) + ).thenReturn(expectedImageWireframe) // When - val wireframes = testedMapper.map(mockImageView, mockMappingContext, mockCallback) + val wireframes = testedMapper.map(mockImageView, mockMappingContext, mockAsyncJobStatusCallback) // Then - assertThat(wireframes.size).isEqualTo(2) - assertThat(wireframes[0]).isEqualTo(expectedWireframe) + assertThat(wireframes.size).isEqualTo(1) + assertThat(wireframes[0]).isEqualTo(expectedImageWireframe) - val argumentCaptor = argumentCaptor() - verify(mockImageWireframeHelper, times(2)) + verify(mockImageWireframeHelper) .createImageWireframe( view = any(), currentWireframeIndex = any(), @@ -257,28 +297,21 @@ internal class ImageViewMapperTest { width = any(), height = any(), usePIIPlaceholder = any(), - drawable = anyOrNull(), + drawable = any(), + asyncJobStatusCallback = eq(mockAsyncJobStatusCallback), + clipping = anyOrNull(), shapeStyle = anyOrNull(), border = anyOrNull(), - clipping = anyOrNull(), - imageWireframeHelperCallback = argumentCaptor.capture(), prefix = anyOrNull() ) - - argumentCaptor.allValues.forEach { - it.onStart() - it.onFinished() - } - verify(mockCallback, times(2)).jobFinished() - verify(mockCallback, times(2)).jobStarted() - verifyNoMoreInteractions(mockCallback) } @Test - fun `M set index to 1 W map() { has background wireframe }`( + fun `M return background of type ImageWireframe W map() { no shape style or border }`( @LongForgery id: Long ) { // Given + whenever(mockImageView.background).thenReturn(mockBackgroundDrawable) val expectedBackgroundWireframe = MobileSegment.Wireframe.ImageWireframe( id = id, x = mockGlobalBounds.x, @@ -290,99 +323,26 @@ internal class ImageViewMapperTest { mimeType = fakeMimeType, isEmpty = true ) - whenever(mockImageView.background).thenReturn(mockBackground) - mockCreateImageWireframe( - expectedBackgroundWireframe, - expectedWireframe + mockImageWireframeHelper( + expectedView = mockImageView, + expectedDrawable = mockBackgroundDrawable, + expectedIndex = 0, + expectedPrefix = BaseAsyncBackgroundWireframeMapper.PREFIX_BACKGROUND_DRAWABLE, + expectedUsePIIPlaceholder = false, + returnedWireframe = expectedBackgroundWireframe ) - - // When - testedMapper.map(mockImageView, mockMappingContext) - - // Then - val captor = argumentCaptor() - verify(mockImageWireframeHelper, times(2)).createImageWireframe( - view = any(), - currentWireframeIndex = captor.capture(), - x = any(), - y = any(), - width = any(), - height = any(), - usePIIPlaceholder = any(), - drawable = anyOrNull(), - shapeStyle = anyOrNull(), - border = anyOrNull(), - clipping = anyOrNull(), - prefix = anyOrNull(), - imageWireframeHelperCallback = anyOrNull() - ) - val allValues = captor.allValues - assertThat(allValues[0]).isEqualTo(0) - assertThat(allValues[1]).isEqualTo(1) - } - - @Test - fun `M set index to 0 W map() { no background wireframe }`() { - // Given - whenever(mockImageView.background).thenReturn(mockBackground) - - mockCreateImageWireframe( - null, - expectedWireframe + mockImageWireframeHelper( + expectedView = mockImageView, + expectedDrawable = mockDrawable, + expectedIndex = 1, + expectedPrefix = ImageWireframeHelper.DRAWABLE_CHILD_NAME, + expectedUsePIIPlaceholder = true, + returnedWireframe = expectedImageWireframe ) // When - testedMapper.map(mockImageView, mockMappingContext) - - // Then - val captor = argumentCaptor() - verify(mockImageWireframeHelper, times(2)).createImageWireframe( - view = any(), - currentWireframeIndex = captor.capture(), - x = any(), - y = any(), - width = any(), - height = any(), - usePIIPlaceholder = any(), - drawable = anyOrNull(), - shapeStyle = anyOrNull(), - border = anyOrNull(), - clipping = anyOrNull(), - prefix = anyOrNull(), - imageWireframeHelperCallback = anyOrNull() - ) - val allValues = captor.allValues - assertThat(allValues[0]).isEqualTo(0) - assertThat(allValues[1]).isEqualTo(0) - } - - @Test - fun `M return background of type ImageWireframe W map() { no shapestyle or border }`( - @LongForgery id: Long - ) { - // Given - whenever(mockImageView.background).thenReturn(mockBackground) - - val expectedBackgroundWireframe = MobileSegment.Wireframe.ImageWireframe( - id = id, - x = mockGlobalBounds.x, - y = mockGlobalBounds.y, - width = mockImageView.width.toLong(), - height = mockImageView.height.toLong(), - shapeStyle = null, - border = null, - mimeType = fakeMimeType, - isEmpty = true - ) - - mockCreateImageWireframe( - expectedBackgroundWireframe, - expectedWireframe - ) - - // When - val wireframes = testedMapper.map(mockImageView, mockMappingContext) + val wireframes = testedMapper.map(mockImageView, mockMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes[0]::class.java).isEqualTo(MobileSegment.Wireframe.ImageWireframe::class.java) @@ -390,13 +350,15 @@ internal class ImageViewMapperTest { @Test fun `M return background of type ShapeWireframe W map() { has shapestyle or border }`( - @Mock mockColorDrawable: ColorDrawable + @Mock mockColorDrawable: ColorDrawable, + @IntForgery mockColor: Int ) { // Given whenever(mockImageView.background).thenReturn(mockColorDrawable) + whenever(mockDrawableToColorMapper.mapDrawableToColor(mockColorDrawable)) doReturn mockColor // When - val wireframes = testedMapper.map(mockImageView, mockMappingContext) + val wireframes = testedMapper.map(mockImageView, mockMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes[0]::class.java).isEqualTo(MobileSegment.Wireframe.ShapeWireframe::class.java) @@ -408,44 +370,50 @@ internal class ImageViewMapperTest { ) { // Given whenever(mockImageView.background).thenReturn(mockColorDrawable) - - whenever(mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier(any(), any())) + whenever(mockViewIdentifierResolver.resolveChildUniqueIdentifier(any(), any())) .thenReturn(null) - - mockCreateImageWireframe( - expectedWireframe, - null + mockImageWireframeHelper( + expectedView = mockImageView, + expectedDrawable = mockDrawable, + expectedIndex = 0, + expectedPrefix = ImageWireframeHelper.DRAWABLE_CHILD_NAME, + expectedUsePIIPlaceholder = true, + returnedWireframe = expectedImageWireframe ) // When - val wireframes = testedMapper.map(mockImageView, mockMappingContext) + val wireframes = testedMapper.map(mockImageView, mockMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes.size).isEqualTo(1) + assertThat(wireframes[0]).isEqualTo(expectedImageWireframe) } - private fun mockCreateImageWireframe( - expectedFirstWireframe: MobileSegment.Wireframe.ImageWireframe?, - expectedSecondWireframe: MobileSegment.Wireframe.ImageWireframe? + private fun mockImageWireframeHelper( + expectedView: View, + expectedDrawable: Drawable, + expectedIndex: Int, + expectedPrefix: String?, + expectedUsePIIPlaceholder: Boolean, + returnedWireframe: MobileSegment.Wireframe ) { whenever( mockImageWireframeHelper.createImageWireframe( - view = any(), - currentWireframeIndex = any(), + view = eq(expectedView), + currentWireframeIndex = eq(expectedIndex), x = any(), y = any(), width = any(), height = any(), - usePIIPlaceholder = any(), - drawable = anyOrNull(), + usePIIPlaceholder = eq(expectedUsePIIPlaceholder), + drawable = eq(expectedDrawable), + asyncJobStatusCallback = eq(mockAsyncJobStatusCallback), + clipping = anyOrNull(), shapeStyle = anyOrNull(), border = anyOrNull(), - clipping = anyOrNull(), - prefix = anyOrNull(), - imageWireframeHelperCallback = anyOrNull() + prefix = eq(expectedPrefix) ) ) - .thenReturn(expectedFirstWireframe) - .thenReturn(expectedSecondWireframe) + .thenReturn(returnedWireframe) } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckBoxMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckBoxMapperTest.kt index eb2842adc5..f77b6df8f5 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckBoxMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckBoxMapperTest.kt @@ -28,9 +28,11 @@ internal class MaskCheckBoxMapperTest : BaseCheckBoxMapperTest() { override fun setupTestedMapper(): CheckBoxMapper { return MaskCheckBoxMapper( - textWireframeMapper = mockTextWireframeMapper, - uniqueIdentifierGenerator = mockuniqueIdentifierGenerator, - viewUtils = mockViewUtils + mockTextWireframeMapper, + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper ) } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckedTextViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckedTextViewMapperTest.kt index d7bc1a9072..4a685b2c39 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckedTextViewMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskCheckedTextViewMapperTest.kt @@ -28,9 +28,11 @@ internal class MaskCheckedTextViewMapperTest : BaseCheckedTextViewMapperTest() { override fun setupTestedMapper(): CheckedTextViewMapper { return MaskCheckedTextViewMapper( - textWireframeMapper = mockTextWireframeMapper, - uniqueIdentifierGenerator = mockuniqueIdentifierGenerator, - viewUtils = mockViewUtils + mockTextWireframeMapper, + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper ) } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskInputTextViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskInputTextViewMapperTest.kt new file mode 100644 index 0000000000..5b0f3277aa --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskInputTextViewMapperTest.kt @@ -0,0 +1,56 @@ +/* + * 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.sessionreplay.internal.recorder.mapper + +import com.datadog.android.sessionreplay.forge.ForgeConfigurator +import com.datadog.android.sessionreplay.internal.recorder.obfuscator.rules.MaskInputObfuscationRule +import com.datadog.tools.unit.extensions.ApiLevelExtension +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 +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(ApiLevelExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(ForgeConfigurator::class) +internal class MaskInputTextViewMapperTest : BaseTextViewWireframeMapperTest() { + + override fun initTestedMapper(): TextViewMapper { + return MaskTextViewMapper( + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper + ).apply { + textValueObfuscationRule = mockObfuscationRule + } + } + + @Test + fun `M use the MaskInputObfuscationRule as defaultObfuscator when initialized`() { + // When + val textViewMapper = MaskInputTextViewMapper( + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper + ) + + // Then + assertThat(textViewMapper.textValueObfuscationRule) + .isInstanceOf(MaskInputObfuscationRule::class.java) + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskNumberPickerMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskNumberPickerMapperTest.kt index 991a384dbb..d23210f13f 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskNumberPickerMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskNumberPickerMapperTest.kt @@ -29,9 +29,10 @@ internal class MaskNumberPickerMapperTest : BaseNumberPickerMapperTest() { override fun provideTestInstance(): BasePickerMapper { return MaskNumberPickerMapper( - mockStringUtils, - mockViewUtils, - mockUniqueIdentifierGenerator + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper ) } @@ -44,7 +45,7 @@ internal class MaskNumberPickerMapperTest : BaseNumberPickerMapperTest() { .copy(text = expectedSelectedLabelValue) val expectedBottomDividerWireframe = fakeBottomDividerWireframe() // When - val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext) + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes).isEqualTo( @@ -67,7 +68,7 @@ internal class MaskNumberPickerMapperTest : BaseNumberPickerMapperTest() { .copy(text = expectedSelectedLabelValue) val expectedBottomDividerWireframe = fakeBottomDividerWireframe() // When - val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext) + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes).isEqualTo( @@ -91,7 +92,7 @@ internal class MaskNumberPickerMapperTest : BaseNumberPickerMapperTest() { val expectedBottomDividerWireframe = fakeBottomDividerWireframe() // When - val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext) + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes).isEqualTo( @@ -117,7 +118,7 @@ internal class MaskNumberPickerMapperTest : BaseNumberPickerMapperTest() { .copy(text = expectedSelectedLabelValue) val expectedBottomDividerWireframe = fakeBottomDividerWireframe() // When - val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext) + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes).isEqualTo( @@ -145,7 +146,7 @@ internal class MaskNumberPickerMapperTest : BaseNumberPickerMapperTest() { .copy(text = expectedSelectedLabelValue) val expectedBottomDividerWireframe = fakeBottomDividerWireframe() // When - val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext) + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes).isEqualTo( @@ -174,7 +175,7 @@ internal class MaskNumberPickerMapperTest : BaseNumberPickerMapperTest() { val expectedBottomDividerWireframe = fakeBottomDividerWireframe() // When - val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext) + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes).isEqualTo( diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskRadioButtonMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskRadioButtonMapperTest.kt index c2f1c6b6a6..139eb4921f 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskRadioButtonMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskRadioButtonMapperTest.kt @@ -28,9 +28,11 @@ internal class MaskRadioButtonMapperTest : BaseRadioButtonMapperTest() { override fun setupTestedMapper(): RadioButtonMapper { return MaskRadioButtonMapper( - textWireframeMapper = mockTextWireframeMapper, - uniqueIdentifierGenerator = mockuniqueIdentifierGenerator, - viewUtils = mockViewUtils + mockTextWireframeMapper, + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper ) } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSeekBarWireframeMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSeekBarWireframeMapperTest.kt index 1ff3bc2578..343dc95712 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSeekBarWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSeekBarWireframeMapperTest.kt @@ -34,9 +34,10 @@ internal class MaskSeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() override fun provideTestInstance(): SeekBarWireframeMapper { return MaskSeekBarWireframeMapper( - viewUtils = mockViewUtils, - stringUtils = mockStringUtils, - uniqueIdentifierGenerator = mockUniqueIdentifierGenerator + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper ) } @@ -56,7 +57,11 @@ internal class MaskSeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() ) // When - val mappedWireframes = testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext) + val mappedWireframes = testedSeekBarWireframeMapper.map( + mockSeekBar, + fakeMappingContext, + mockAsyncJobStatusCallback + ) // Then assertThat(mappedWireframes).isEqualTo( @@ -71,7 +76,7 @@ internal class MaskSeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() // Given val fakeDefaultDayNotActiveHtmlColor = forge.aStringMatching("#[0-9A-Fa-f]{8}") whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( SeekBarWireframeMapper.DAY_MODE_COLOR, SeekBarWireframeMapper.PARTIALLY_OPAQUE_ALPHA_VALUE ) @@ -103,7 +108,11 @@ internal class MaskSeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() ) // When - val mappedWireframes = testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext) + val mappedWireframes = testedSeekBarWireframeMapper.map( + mockSeekBar, + fakeMappingContext, + mockAsyncJobStatusCallback + ) // Then assertThat(mappedWireframes).isEqualTo(listOf(expectedInactiveTrackWireframe)) @@ -114,7 +123,7 @@ internal class MaskSeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() // Given val fakeDefaultNightNotActiveHtmlColor = forge.aStringMatching("#[0-9A-Fa-f]{8}") whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( SeekBarWireframeMapper.NIGHT_MODE_COLOR, SeekBarWireframeMapper.PARTIALLY_OPAQUE_ALPHA_VALUE ) @@ -146,7 +155,11 @@ internal class MaskSeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() ) // When - val mappedWireframes = testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext) + val mappedWireframes = testedSeekBarWireframeMapper.map( + mockSeekBar, + fakeMappingContext, + mockAsyncJobStatusCallback + ) // Then assertThat(mappedWireframes).isEqualTo(listOf(expectedInactiveTrackWireframe)) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSwitchCompatMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSwitchCompatMapperTest.kt index 7f736997e4..0aac9020b3 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSwitchCompatMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskSwitchCompatMapperTest.kt @@ -8,7 +8,6 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -31,8 +30,10 @@ internal class MaskSwitchCompatMapperTest : BaseSwitchCompatMapperTest() { override fun setupTestedMapper(): SwitchCompatMapper { return MaskSwitchCompatMapper( mockTextWireframeMapper, - uniqueIdentifierGenerator = mockuniqueIdentifierGenerator, - viewUtils = mockViewUtils + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper ) } @@ -40,10 +41,6 @@ internal class MaskSwitchCompatMapperTest : BaseSwitchCompatMapperTest() { fun `M resolve the switch as wireframes W map() { checked }`() { // Given whenever(mockSwitch.isChecked).thenReturn(true) - val expectedColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val expectedThumbWidth = normalizedThumbWidth - normalizedThumbRightPadding - normalizedThumbLeftPadding val expectedTrackWidth = expectedThumbWidth * 2 @@ -57,7 +54,7 @@ internal class MaskSwitchCompatMapperTest : BaseSwitchCompatMapperTest() { height = expectedTrackHeight, border = null, shapeStyle = MobileSegment.ShapeStyle( - backgroundColor = expectedColor, + backgroundColor = fakeCurrentTextColorString, mockSwitch.alpha ) ) @@ -65,7 +62,8 @@ internal class MaskSwitchCompatMapperTest : BaseSwitchCompatMapperTest() { // When val resolvedWireframes = testedSwitchCompatMapper.map( mockSwitch, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -76,10 +74,6 @@ internal class MaskSwitchCompatMapperTest : BaseSwitchCompatMapperTest() { fun `M resolve the switch as wireframes W map() { not checked }`() { // Given whenever(mockSwitch.isChecked).thenReturn(false) - val expectedColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val expectedThumbWidth = normalizedThumbWidth - normalizedThumbRightPadding - normalizedThumbLeftPadding val expectedTrackWidth = expectedThumbWidth * 2 @@ -93,7 +87,7 @@ internal class MaskSwitchCompatMapperTest : BaseSwitchCompatMapperTest() { height = expectedTrackHeight, border = null, shapeStyle = MobileSegment.ShapeStyle( - backgroundColor = expectedColor, + backgroundColor = fakeCurrentTextColorString, mockSwitch.alpha ) ) @@ -101,7 +95,8 @@ internal class MaskSwitchCompatMapperTest : BaseSwitchCompatMapperTest() { // When val resolvedWireframes = testedSwitchCompatMapper.map( mockSwitch, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskTextViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskTextViewMapperTest.kt index 8e72cc53d7..a0254d50b9 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskTextViewMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/MaskTextViewMapperTest.kt @@ -30,16 +30,24 @@ internal class MaskTextViewMapperTest : BaseTextViewWireframeMapperTest() { override fun initTestedMapper(): TextViewMapper { return MaskTextViewMapper( - imageWireframeHelper = mockImageWireframeHelper, - uniqueIdentifierGenerator = mockUniqueIdentifierGenerator, + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper + ).apply { textValueObfuscationRule = mockObfuscationRule - ) + } } @Test fun `M use the MaskObfuscationRule as defaultObfuscator when initialized`() { // When - val textViewMapper = MaskTextViewMapper() + val textViewMapper = MaskTextViewMapper( + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper + ) // Then assertThat(textViewMapper.textValueObfuscationRule) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/NumberPickerMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/NumberPickerMapperTest.kt index 6b56841da1..e42a938469 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/NumberPickerMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/NumberPickerMapperTest.kt @@ -28,7 +28,12 @@ import org.mockito.quality.Strictness internal class NumberPickerMapperTest : BaseNumberPickerMapperTest() { override fun provideTestInstance(): NumberPickerMapper { - return NumberPickerMapper(mockStringUtils, mockViewUtils, mockUniqueIdentifierGenerator) + return NumberPickerMapper( + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper + ) } @Test @@ -47,7 +52,7 @@ internal class NumberPickerMapperTest : BaseNumberPickerMapperTest() { .copy(text = expectedNextLabelValue) // When - val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext) + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes).isEqualTo( @@ -79,7 +84,7 @@ internal class NumberPickerMapperTest : BaseNumberPickerMapperTest() { .copy(text = expectedNextLabelValue) // When - val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext) + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes).isEqualTo( @@ -111,7 +116,7 @@ internal class NumberPickerMapperTest : BaseNumberPickerMapperTest() { .copy(text = expectedNextLabelValue) // When - val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext) + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes).isEqualTo( @@ -147,7 +152,7 @@ internal class NumberPickerMapperTest : BaseNumberPickerMapperTest() { .copy(text = expectedNextLabelValue) // When - val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext) + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes).isEqualTo( @@ -184,7 +189,7 @@ internal class NumberPickerMapperTest : BaseNumberPickerMapperTest() { .copy(text = expectedNextLabelValue) // When - val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext) + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes).isEqualTo( @@ -222,7 +227,7 @@ internal class NumberPickerMapperTest : BaseNumberPickerMapperTest() { .copy(text = expectedNextLabelValue) // When - val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext) + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) // Then assertThat(wireframes).isEqualTo( @@ -240,29 +245,37 @@ internal class NumberPickerMapperTest : BaseNumberPickerMapperTest() { fun `M return empty list W map { prevLabelId null }`() { // Given whenever( - mockUniqueIdentifierGenerator + mockViewIdentifierResolver .resolveChildUniqueIdentifier( mockNumberPicker, BasePickerMapper.PREV_INDEX_KEY_NAME ) ) .thenReturn(null) + + // When + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) + // Then - assertThat(testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext)).isEmpty() + assertThat(wireframes).isEmpty() } @Test fun `M return empty list W map { nextLabelId null }`() { // Given whenever( - mockUniqueIdentifierGenerator + mockViewIdentifierResolver .resolveChildUniqueIdentifier( mockNumberPicker, BasePickerMapper.NEXT_INDEX_KEY_NAME ) ) .thenReturn(null) + + // When + val wireframes = testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext, mockAsyncJobStatusCallback) + // Then - assertThat(testedNumberPickerMapper.map(mockNumberPicker, fakeMappingContext)).isEmpty() + assertThat(wireframes).isEmpty() } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/RadioButtonMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/RadioButtonMapperTest.kt index 771fb93a94..be2d46d526 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/RadioButtonMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/RadioButtonMapperTest.kt @@ -27,9 +27,11 @@ internal class RadioButtonMapperTest : BaseRadioButtonMapperTest() { override fun setupTestedMapper(): RadioButtonMapper { return RadioButtonMapper( - textWireframeMapper = mockTextWireframeMapper, - uniqueIdentifierGenerator = mockuniqueIdentifierGenerator, - viewUtils = mockViewUtils + mockTextWireframeMapper, + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper ) } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapperTest.kt index 31770b62c7..4f16f648cb 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapperTest.kt @@ -34,9 +34,10 @@ internal class SeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() { override fun provideTestInstance(): SeekBarWireframeMapper { return SeekBarWireframeMapper( - viewUtils = mockViewUtils, - stringUtils = mockStringUtils, - uniqueIdentifierGenerator = mockUniqueIdentifierGenerator + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper ) } @@ -79,7 +80,11 @@ internal class SeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() { ) // When - val mappedWireframes = testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext) + val mappedWireframes = testedSeekBarWireframeMapper.map( + mockSeekBar, + fakeMappingContext, + mockAsyncJobStatusCallback + ) // Then assertThat(mappedWireframes).isEqualTo( @@ -97,13 +102,13 @@ internal class SeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() { val fakeDefaultDayHtmlColor = forge.aStringMatching("#[0-9A-Fa-f]{8}") val fakeDefaultDayNotActiveHtmlColor = forge.aStringMatching("#[0-9A-Fa-f]{8}") whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( SeekBarWireframeMapper.DAY_MODE_COLOR, SeekBarWireframeMapper.OPAQUE_ALPHA_VALUE ) ).thenReturn(fakeDefaultDayHtmlColor) whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( SeekBarWireframeMapper.DAY_MODE_COLOR, SeekBarWireframeMapper.PARTIALLY_OPAQUE_ALPHA_VALUE ) @@ -158,7 +163,11 @@ internal class SeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() { ) // When - val mappedWireframes = testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext) + val mappedWireframes = testedSeekBarWireframeMapper.map( + mockSeekBar, + fakeMappingContext, + mockAsyncJobStatusCallback + ) // Then assertThat(mappedWireframes).isEqualTo( @@ -176,13 +185,13 @@ internal class SeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() { val fakeDefaultNightHtmlColor = forge.aStringMatching("#[0-9A-Fa-f]{8}") val fakeDefaultNightNotActiveHtmlColor = forge.aStringMatching("#[0-9A-Fa-f]{8}") whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( SeekBarWireframeMapper.NIGHT_MODE_COLOR, SeekBarWireframeMapper.OPAQUE_ALPHA_VALUE ) ).thenReturn(fakeDefaultNightHtmlColor) whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( SeekBarWireframeMapper.NIGHT_MODE_COLOR, SeekBarWireframeMapper.PARTIALLY_OPAQUE_ALPHA_VALUE ) @@ -237,7 +246,11 @@ internal class SeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() { ) // When - val mappedWireframes = testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext) + val mappedWireframes = testedSeekBarWireframeMapper.map( + mockSeekBar, + fakeMappingContext, + mockAsyncJobStatusCallback + ) // Then assertThat(mappedWireframes).isEqualTo( @@ -254,7 +267,7 @@ internal class SeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() { // Given val fakeDefaultDayHtmlColor = forge.aStringMatching("#[0-9A-Fa-f]{8}") whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( SeekBarWireframeMapper.DAY_MODE_COLOR, SeekBarWireframeMapper.OPAQUE_ALPHA_VALUE ) @@ -308,7 +321,11 @@ internal class SeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() { ) // When - val mappedWireframes = testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext) + val mappedWireframes = testedSeekBarWireframeMapper.map( + mockSeekBar, + fakeMappingContext, + mockAsyncJobStatusCallback + ) // Then assertThat(mappedWireframes).isEqualTo( @@ -325,7 +342,7 @@ internal class SeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() { // Given val fakeDefaultNightHtmlColor = forge.aStringMatching("#[0-9A-Fa-f]{8}") whenever( - mockStringUtils.formatColorAndAlphaAsHexa( + mockColorStringFormatter.formatColorAndAlphaAsHexString( SeekBarWireframeMapper.NIGHT_MODE_COLOR, SeekBarWireframeMapper.OPAQUE_ALPHA_VALUE ) @@ -379,7 +396,11 @@ internal class SeekBarWireframeMapperTest : BaseSeekBarWireframeMapperTest() { ) // When - val mappedWireframes = testedSeekBarWireframeMapper.map(mockSeekBar, fakeMappingContext) + val mappedWireframes = testedSeekBarWireframeMapper.map( + mockSeekBar, + fakeMappingContext, + mockAsyncJobStatusCallback + ) // Then assertThat(mappedWireframes).isEqualTo( diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapperTest.kt index e1b6546a07..7e9fa6c7a5 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapperTest.kt @@ -8,7 +8,6 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.StringUtils import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension @@ -32,8 +31,10 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { override fun setupTestedMapper(): SwitchCompatMapper { return SwitchCompatMapper( mockTextWireframeMapper, - uniqueIdentifierGenerator = mockuniqueIdentifierGenerator, - viewUtils = mockViewUtils + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper ) } @@ -41,10 +42,6 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { fun `M resolve the switch as wireframes W map() { checked }`() { // Given whenever(mockSwitch.isChecked).thenReturn(true) - val expectedColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val expectedThumbWidth = normalizedThumbWidth - normalizedThumbRightPadding - normalizedThumbLeftPadding val expectedTrackWidth = expectedThumbWidth * 2 @@ -58,7 +55,7 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { height = expectedTrackHeight, border = null, shapeStyle = MobileSegment.ShapeStyle( - backgroundColor = expectedColor, + backgroundColor = fakeCurrentTextColorString, mockSwitch.alpha ) ) @@ -70,7 +67,7 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { height = expectedThumbWidth, border = null, shapeStyle = MobileSegment.ShapeStyle( - backgroundColor = expectedColor, + backgroundColor = fakeCurrentTextColorString, mockSwitch.alpha, cornerRadius = SwitchCompatMapper.THUMB_CORNER_RADIUS ) @@ -79,7 +76,8 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { // When val resolvedWireframes = testedSwitchCompatMapper.map( mockSwitch, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -91,10 +89,6 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { fun `M resolve the switch as wireframes W map() { not checked }`() { // Given whenever(mockSwitch.isChecked).thenReturn(false) - val expectedColor = StringUtils.formatColorAndAlphaAsHexa( - fakeCurrentTextColor, - OPAQUE_ALPHA_VALUE - ) val expectedThumbWidth = normalizedThumbWidth - normalizedThumbRightPadding - normalizedThumbLeftPadding val expectedTrackWidth = expectedThumbWidth * 2 @@ -108,7 +102,7 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { height = expectedTrackHeight, border = null, shapeStyle = MobileSegment.ShapeStyle( - backgroundColor = expectedColor, + backgroundColor = fakeCurrentTextColorString, mockSwitch.alpha ) ) @@ -120,7 +114,7 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { height = expectedThumbWidth, border = null, shapeStyle = MobileSegment.ShapeStyle( - backgroundColor = expectedColor, + backgroundColor = fakeCurrentTextColorString, mockSwitch.alpha, cornerRadius = SwitchCompatMapper.THUMB_CORNER_RADIUS ) @@ -129,7 +123,8 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { // When val resolvedWireframes = testedSwitchCompatMapper.map( mockSwitch, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then @@ -143,7 +138,7 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { ) { // Given whenever( - mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockSwitch, SwitchCompatMapper.THUMB_KEY_NAME ) @@ -153,7 +148,8 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { // When val resolvedWireframes = testedSwitchCompatMapper.map( mockSwitch, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback ) // Then diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/TextViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/TextViewMapperTest.kt index 7e8d2dde6f..7ec2016342 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/TextViewMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/TextViewMapperTest.kt @@ -28,10 +28,27 @@ import org.mockito.quality.Strictness @ForgeConfiguration(ForgeConfigurator::class) internal class TextViewMapperTest : BaseTextViewWireframeMapperTest() { + override fun initTestedMapper(): TextViewMapper { + return TextViewMapper( + mockObfuscationRule, + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper + ).apply { + textValueObfuscationRule = mockObfuscationRule + } + } + @Test fun `M use the AllowObfuscationRule when initialized`() { // When - val textViewMapper = TextViewMapper() + val textViewMapper = TextViewMapper( + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper + ) // Then assertThat(textViewMapper.textValueObfuscationRule) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/UnsupportedViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/UnsupportedViewMapperTest.kt index 27d431ae03..9c3fcaa967 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/UnsupportedViewMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/UnsupportedViewMapperTest.kt @@ -10,13 +10,13 @@ import android.view.View import android.widget.Toolbar import androidx.appcompat.widget.ActionBarContainer import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.optionselectormocks.AppcompatToolbarCustomSubclass import com.datadog.android.sessionreplay.internal.recorder.optionselectormocks.ToolbarCustomSubclass import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.ViewUtils +import com.datadog.android.sessionreplay.utils.GlobalBounds import com.datadog.tools.unit.extensions.ApiLevelExtension import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.LongForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -27,6 +27,8 @@ 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.doReturn import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import androidx.appcompat.widget.Toolbar as AppCompatToolbar @@ -38,7 +40,7 @@ import androidx.appcompat.widget.Toolbar as AppCompatToolbar ) @MockitoSettings(strictness = Strictness.LENIENT) @ForgeConfiguration(ForgeConfigurator::class) -internal class UnsupportedViewMapperTest : BaseTextViewWireframeMapperTest() { +internal class UnsupportedViewMapperTest : BaseWireframeMapperTest() { private lateinit var testedUnsupportedViewMapper: UnsupportedViewMapper @@ -57,53 +59,60 @@ internal class UnsupportedViewMapperTest : BaseTextViewWireframeMapperTest() { @Mock lateinit var mockAppcompatSubclass: AppcompatToolbarCustomSubclass - @Mock - lateinit var mockViewUtils: ViewUtils - @Forgery lateinit var fakeViewGlobalBounds: GlobalBounds + @LongForgery + var fakeWireframeId: Long = 0L + @BeforeEach fun setup() { whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockAppCompatToolbar, fakeMappingContext.systemInformation.screenDensity ) ).thenReturn(fakeViewGlobalBounds) whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockToolbarSubclass, fakeMappingContext.systemInformation.screenDensity ) ).thenReturn(fakeViewGlobalBounds) whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockToolbar, fakeMappingContext.systemInformation.screenDensity ) ).thenReturn(fakeViewGlobalBounds) whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockActionBarContainer, fakeMappingContext.systemInformation.screenDensity ) ).thenReturn(fakeViewGlobalBounds) whenever( - mockViewUtils.resolveViewGlobalBounds( + mockViewBoundsResolver.resolveViewGlobalBounds( mockAppcompatSubclass, fakeMappingContext.systemInformation.screenDensity ) ).thenReturn(fakeViewGlobalBounds) - testedUnsupportedViewMapper = UnsupportedViewMapper(mockViewUtils) + whenever(mockViewIdentifierResolver.resolveViewId(any())) doReturn fakeWireframeId + + testedUnsupportedViewMapper = UnsupportedViewMapper( + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper + ) } @Test fun `M resolve with the toolbar label as text W map { AppCompatToolbar }`() { // Given val expectedWireframe = MobileSegment.Wireframe.PlaceholderWireframe( - id = System.identityHashCode(mockAppCompatToolbar).toLong(), + id = fakeWireframeId, x = fakeViewGlobalBounds.x, y = fakeViewGlobalBounds.y, width = fakeViewGlobalBounds.width, @@ -121,7 +130,7 @@ internal class UnsupportedViewMapperTest : BaseTextViewWireframeMapperTest() { fun `M resolve with the toolbar label as text W map { Subclass of AppCompatToolbar }`() { // Given val expectedWireframe = MobileSegment.Wireframe.PlaceholderWireframe( - id = System.identityHashCode(mockAppcompatSubclass).toLong(), + id = fakeWireframeId, x = fakeViewGlobalBounds.x, y = fakeViewGlobalBounds.y, width = fakeViewGlobalBounds.width, @@ -140,7 +149,7 @@ internal class UnsupportedViewMapperTest : BaseTextViewWireframeMapperTest() { fun `M resolve with the toolbar label as text W map { Subclass of Toolbar }`() { // Given val expectedWireframe = MobileSegment.Wireframe.PlaceholderWireframe( - id = System.identityHashCode(mockToolbarSubclass).toLong(), + id = fakeWireframeId, x = fakeViewGlobalBounds.x, y = fakeViewGlobalBounds.y, width = fakeViewGlobalBounds.width, @@ -159,7 +168,7 @@ internal class UnsupportedViewMapperTest : BaseTextViewWireframeMapperTest() { fun `M resolve with the toolbar label as text W map { Toolbar }`() { // Given val expectedWireframe = MobileSegment.Wireframe.PlaceholderWireframe( - id = System.identityHashCode(mockToolbar).toLong(), + id = fakeWireframeId, x = fakeViewGlobalBounds.x, y = fakeViewGlobalBounds.y, width = fakeViewGlobalBounds.width, @@ -178,7 +187,7 @@ internal class UnsupportedViewMapperTest : BaseTextViewWireframeMapperTest() { fun `M resolve with the default label as text W map { default unsupported view }`() { // Given val expectedWireframe = MobileSegment.Wireframe.PlaceholderWireframe( - id = System.identityHashCode(mockActionBarContainer).toLong(), + id = fakeWireframeId, x = fakeViewGlobalBounds.x, y = fakeViewGlobalBounds.y, width = fakeViewGlobalBounds.width, @@ -198,7 +207,8 @@ internal class UnsupportedViewMapperTest : BaseTextViewWireframeMapperTest() { private fun getWireframe(view: View): MobileSegment.Wireframe.PlaceholderWireframe { return testedUnsupportedViewMapper.map( view, - fakeMappingContext + fakeMappingContext, + mockAsyncJobStatusCallback )[0] } // endregion diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewScreenshotWireframeMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewScreenshotWireframeMapperTest.kt deleted file mode 100644 index bc75129e7f..0000000000 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewScreenshotWireframeMapperTest.kt +++ /dev/null @@ -1,53 +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.sessionreplay.internal.recorder.mapper - -import android.view.View -import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.aMockView -import com.datadog.android.sessionreplay.model.MobileSegment -import fr.xgouchet.elmyr.Forge -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.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(ForgeConfigurator::class) -internal class ViewScreenshotWireframeMapperTest : BaseWireframeMapperTest() { - - lateinit var testedWireframeMapper: ViewScreenshotWireframeMapper - - @BeforeEach - fun `set up`() { - testedWireframeMapper = ViewScreenshotWireframeMapper() - } - - @Test - fun `M resolve a ShapeWireframe with border W map()`(forge: Forge) { - // Given - val mockView: View = forge.aMockView() - // When - val shapeWireframes = testedWireframeMapper.map(mockView, fakeMappingContext) - - // Then - val expectedWireframes = mockView.toShapeWireframes().map { - it.copy(border = MobileSegment.ShapeBorder("#000000ff", 1)) - } - assertThat(shapeWireframes).isEqualTo(expectedWireframes) - } -} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewWireframeMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewWireframeMapperTest.kt index 23452e9f18..39cdd62614 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewWireframeMapperTest.kt @@ -7,18 +7,17 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.graphics.drawable.ColorDrawable -import android.graphics.drawable.Drawable -import android.graphics.drawable.InsetDrawable -import android.graphics.drawable.RippleDrawable -import android.os.Build import android.view.View -import androidx.annotation.RequiresApi import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.sessionreplay.internal.recorder.aMockView import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.tools.unit.annotations.TestTargetApi +import com.datadog.android.sessionreplay.utils.GlobalBounds import com.datadog.tools.unit.extensions.ApiLevelExtension import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -28,6 +27,7 @@ import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -43,236 +43,92 @@ internal class ViewWireframeMapperTest : BaseWireframeMapperTest() { lateinit var testedWireframeMapper: ViewWireframeMapper + @Forgery + lateinit var fakeBounds: GlobalBounds + + @LongForgery + var fakeWireframeId: Long = 0L + @BeforeEach fun `set up`() { - testedWireframeMapper = ViewWireframeMapper() + testedWireframeMapper = ViewWireframeMapper( + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper + ) } @Test fun `M resolve a ShapeWireframe W map()`(forge: Forge) { // Given val mockView: View = forge.aMockView() - val expectedWireframes = mockView.toShapeWireframes() - - // When - val shapeWireframes = testedWireframeMapper.map(mockView, fakeMappingContext) - - // Then - assertThat(shapeWireframes).isEqualTo(expectedWireframes) - } - - @Test - fun `M use the View hashcode for Wireframe id W produce()`(forge: Forge) { - // Given - val mockViews = forge.aList(size = forge.anInt(min = 10, max = 20)) { - aMockView() - } + whenever( + mockViewBoundsResolver.resolveViewGlobalBounds( + mockView, + fakeMappingContext.systemInformation.screenDensity + ) + ) doReturn fakeBounds + whenever(mockViewIdentifierResolver.resolveViewId(mockView)) doReturn fakeWireframeId // When - val shapeWireframes = mockViews.flatMap { - testedWireframeMapper.map( - it, - fakeMappingContext - ) - } + val wireframes = testedWireframeMapper.map(mockView, fakeMappingContext, mockAsyncJobStatusCallback) // Then - val idsSet: MutableSet = mutableSetOf() - shapeWireframes.forEach { - assertThat(idsSet).doesNotContain(it.id) - idsSet.add(it.id) - } + assertThat(wireframes.size).isEqualTo(1) + val wireframe = wireframes.first() + + assertThat(wireframe.id).isEqualTo(fakeWireframeId) + assertThat(wireframe.x).isEqualTo(fakeBounds.x) + assertThat(wireframe.y).isEqualTo(fakeBounds.y) + assertThat(wireframe.width).isEqualTo(fakeBounds.width) + assertThat(wireframe.height).isEqualTo(fakeBounds.height) + assertThat(wireframe.clip).isNull() + assertThat(wireframe.shapeStyle).isNull() + assertThat(wireframe.border).isNull() } @Test fun `M resolve a ShapeWireframe with shapeStyle W map { ColorDrawable }`( - forge: Forge + forge: Forge, + @IntForgery(0, 0xFFFFFF) fakeBackgroundColor: Int, + @StringForgery(regex = "#[0-9A-Z]{8}") fakeBackgroundColorString: String ) { // Given val fakeViewAlpha = forge.aFloat(min = 0f, max = 1f) - val fakeStyleColor = forge.aStringMatching("#[0-9a-f]{8}") - val fakeDrawableColor = fakeStyleColor - .substring(1) - .toLong(16) - .shr(8) - .toInt() - val fakeDrawableAlpha = fakeStyleColor - .substring(1) - .toLong(16) - .and(ALPHA_MASK) - .toInt() - val mockDrawable = mock { - whenever(it.color).thenReturn(fakeDrawableColor) - whenever(it.alpha).thenReturn(fakeDrawableAlpha) - } + val mockDrawable = mock() val mockView = forge.aMockView().apply { whenever(this.background).thenReturn(mockDrawable) whenever(this.alpha).thenReturn(fakeViewAlpha) } - - // When - val shapeWireframes = testedWireframeMapper.map(mockView, fakeMappingContext) - - // Then - val expectedWireframes = mockView.toShapeWireframes().map { - it.copy( - shapeStyle = MobileSegment.ShapeStyle( - backgroundColor = fakeStyleColor, - opacity = fakeViewAlpha, - cornerRadius = null - ) - ) - } - assertThat(shapeWireframes).isEqualTo(expectedWireframes) - } - - @RequiresApi(Build.VERSION_CODES.M) - @TestTargetApi(Build.VERSION_CODES.M) - @Test - fun `M resolve a ShapeWireframe with shapeStyle W map {InsetDrawable, M}`( - forge: Forge - ) { - // Given - val fakeViewAlpha = forge.aFloat(min = 0f, max = 1f) - val fakeStyleColor = forge.aStringMatching("#[0-9a-f]{8}") - val fakeDrawableColor = fakeStyleColor - .substring(1) - .toLong(16) - .shr(8) - .toInt() - val fakeDrawableAlpha = fakeStyleColor - .substring(1) - .toLong(16) - .and(ALPHA_MASK) - .toInt() - val mockDrawable = mock { - whenever(it.color).thenReturn(fakeDrawableColor) - whenever(it.alpha).thenReturn(fakeDrawableAlpha) - } - val mockInsetDrawable = mock { - whenever(it.drawable).thenReturn(mockDrawable) - } - val mockView = forge.aMockView().apply { - whenever(this.background).thenReturn(mockInsetDrawable) - whenever(this.alpha).thenReturn(fakeViewAlpha) - } - - // When - val shapeWireframes = testedWireframeMapper.map(mockView, fakeMappingContext) - - // Then - val expectedWireframes = mockView.toShapeWireframes().map { - it.copy( - shapeStyle = MobileSegment.ShapeStyle( - backgroundColor = fakeStyleColor, - opacity = fakeViewAlpha, - cornerRadius = null - ) + whenever( + mockViewBoundsResolver.resolveViewGlobalBounds( + mockView, + fakeMappingContext.systemInformation.screenDensity ) - } - assertThat(shapeWireframes).isEqualTo(expectedWireframes) - } - - @Test - fun `M resolve a ShapeWireframe no shapeStyle W map { InsetDrawable, lower than M }`( - forge: Forge - ) { - // Given - val fakeStyleColor = forge.aStringMatching("#[0-9a-f]{8}") - val fakeDrawableColor = fakeStyleColor - .substring(1) - .toLong(16) - .shr(8) - .toInt() - val fakeDrawableAlpha = fakeStyleColor - .substring(1) - .toLong(16) - .and(ALPHA_MASK) - .toInt() - val mockDrawable = mock { - whenever(it.color).thenReturn(fakeDrawableColor) - whenever(it.alpha).thenReturn(fakeDrawableAlpha) - } - val mockInsetDrawable = mock { - whenever(it.drawable).thenReturn(mockDrawable) - } - val mockView = forge.aMockView().apply { - whenever(this.background).thenReturn(mockInsetDrawable) - } + ) doReturn fakeBounds + whenever(mockDrawableToColorMapper.mapDrawableToColor(mockDrawable)) doReturn fakeBackgroundColor + whenever(mockColorStringFormatter.formatColorAsHexString(fakeBackgroundColor)) + .doReturn(fakeBackgroundColorString) + whenever(mockViewIdentifierResolver.resolveViewId(mockView)) doReturn fakeWireframeId // When - val shapeWireframes = testedWireframeMapper.map(mockView, fakeMappingContext) + val shapeWireframes = testedWireframeMapper.map(mockView, fakeMappingContext, mockAsyncJobStatusCallback) // Then - val expectedWireframes = mockView.toShapeWireframes() - assertThat(shapeWireframes).isEqualTo(expectedWireframes) - } - - @Test - fun `M resolve a ShapeWireframe with shapeStyle W map {RippleDrawable, ColorDrawable}`( - forge: Forge - ) { - // Given - val fakeViewAlpha = forge.aFloat(min = 0f, max = 1f) - val fakeStyleColor = forge.aStringMatching("#[0-9a-f]{8}") - val fakeDrawableColor = fakeStyleColor - .substring(1) - .toLong(16) - .shr(8) - .toInt() - val fakeDrawableAlpha = fakeStyleColor - .substring(1) - .toLong(16) - .and(ALPHA_MASK) - .toInt() - val mockDrawable = mock { - whenever(it.color).thenReturn(fakeDrawableColor) - whenever(it.alpha).thenReturn(fakeDrawableAlpha) - } - val mockRipple = mock { - whenever(it.numberOfLayers).thenReturn(forge.anInt(min = 1)) - whenever(it.getDrawable(0)).thenReturn(mockDrawable) - } - val mockView = forge.aMockView().apply { - whenever(this.background).thenReturn(mockRipple) - whenever(this.alpha).thenReturn(fakeViewAlpha) - } - - // When - val shapeWireframes = testedWireframeMapper.map(mockView, fakeMappingContext) - - // Then - val expectedWireframes = mockView.toShapeWireframes().map { - it.copy( - shapeStyle = MobileSegment.ShapeStyle( - backgroundColor = fakeStyleColor, - opacity = fakeViewAlpha, - cornerRadius = null - ) + val expectedWireframe = MobileSegment.Wireframe.ShapeWireframe( + id = fakeWireframeId, + x = fakeBounds.x, + y = fakeBounds.y, + width = fakeBounds.width, + height = fakeBounds.height, + shapeStyle = MobileSegment.ShapeStyle( + backgroundColor = fakeBackgroundColorString, + opacity = fakeViewAlpha, + cornerRadius = null ) - } - assertThat(shapeWireframes).isEqualTo(expectedWireframes) - } - - @Test - fun `M resolve a ShapeWireframe no shapeStyle W produce {RippleDrawable, NonColorDrawable}`( - forge: Forge - ) { - // Given - val mockDrawable = mock() - val mockRipple: RippleDrawable = mock { - whenever(it.numberOfLayers).thenReturn(forge.anInt(min = 1)) - whenever(it.getDrawable(0)).thenReturn(mockDrawable) - } - val mockView = forge.aMockView().apply { - whenever(this.background).thenReturn(mockRipple) - } - - // When - val shapeWireframes = testedWireframeMapper.map(mockView, fakeMappingContext) + ) - // Then - val expectedWireframes = mockView.toShapeWireframes() - assertThat(shapeWireframes).isEqualTo(expectedWireframes) + assertThat(shapeWireframes).isEqualTo(listOf(expectedWireframe)) } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/DefaultStringObfuscatorTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/AndroidNStringObfuscatorTest.kt similarity index 55% rename from features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/DefaultStringObfuscatorTest.kt rename to features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/AndroidNStringObfuscatorTest.kt index 390e34dcd3..bcdce23cf5 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/DefaultStringObfuscatorTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/AndroidNStringObfuscatorTest.kt @@ -6,9 +6,7 @@ package com.datadog.android.sessionreplay.internal.recorder.obfuscator -import android.os.Build import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.tools.unit.annotations.TestTargetApi import com.datadog.tools.unit.extensions.ApiLevelExtension import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.junit5.ForgeConfiguration @@ -30,19 +28,18 @@ import java.util.LinkedList ) @MockitoSettings(strictness = Strictness.LENIENT) @ForgeConfiguration(ForgeConfigurator::class) -internal class DefaultStringObfuscatorTest { +internal class AndroidNStringObfuscatorTest { - lateinit var testedObfuscator: DefaultStringObfuscator + lateinit var testedObfuscator: StringObfuscator @BeforeEach fun `set up`() { - testedObfuscator = DefaultStringObfuscator() + testedObfuscator = AndroidNStringObfuscator() } // region Android N and above @Test - @TestTargetApi(Build.VERSION_CODES.N) fun `M mask String W obfuscate(){string with newline, Android N}`( forge: Forge ) { @@ -75,7 +72,6 @@ internal class DefaultStringObfuscatorTest { } @Test - @TestTargetApi(Build.VERSION_CODES.N) fun `M mask String W obfuscate(){string with carriage return character, Android N}`( forge: Forge ) { @@ -108,7 +104,6 @@ internal class DefaultStringObfuscatorTest { } @Test - @TestTargetApi(Build.VERSION_CODES.N) fun `M mask String W obfuscate(){string with whitespace character, Android N}`( forge: Forge ) { @@ -141,7 +136,6 @@ internal class DefaultStringObfuscatorTest { } @Test - @TestTargetApi(Build.VERSION_CODES.N) fun `M mask String W obfuscate(){string with whitespace character and emoji, Android N}`( forge: Forge ) { @@ -168,131 +162,6 @@ internal class DefaultStringObfuscatorTest { // endregion - // region below Android N - - @Test - fun `M mask String W obfuscate(){string with newline}`( - forge: Forge - ) { - // Given - val fakeExpectedChunk1 = forge.aString(size = forge.anInt(1, max = 10)) { '\n' } - val fakeExpectedChunk2 = forge.aString { 'x' } - val fakeExpectedChunk3 = forge.aString(size = forge.anInt(1, max = 10)) { '\n' } - val fakeExpectedChunk4 = forge.aString { 'x' } - val fakeExpectedChunk5 = forge.aString(size = forge.anInt(1, max = 10)) { '\n' } - val fakeExpectedText = ( - fakeExpectedChunk1 + - fakeExpectedChunk2 + - fakeExpectedChunk3 + - fakeExpectedChunk4 + - fakeExpectedChunk5 - ) - val fakeText = ( - fakeExpectedChunk1 + - forge.aString(fakeExpectedChunk2.length) { forge.anAlphaNumericalChar() } + - fakeExpectedChunk3 + - forge.aString(fakeExpectedChunk4.length) { forge.anAlphaNumericalChar() } + - fakeExpectedChunk5 - ) - - // When - val obfuscatedText = testedObfuscator.obfuscate(fakeText) - - // Then - assertThat(obfuscatedText).isEqualTo(fakeExpectedText) - } - - @Test - fun `M mask String W obfuscate(){string with carriage return character}`( - forge: Forge - ) { - // Given - val fakeExpectedChunk1 = forge.aString(size = forge.anInt(1, max = 10)) { '\r' } - val fakeExpectedChunk2 = forge.aString { 'x' } - val fakeExpectedChunk3 = forge.aString(size = forge.anInt(1, max = 10)) { '\r' } - val fakeExpectedChunk4 = forge.aString { 'x' } - val fakeExpectedChunk5 = forge.aString(size = forge.anInt(1, max = 10)) { '\r' } - val fakeExpectedText = ( - fakeExpectedChunk1 + - fakeExpectedChunk2 + - fakeExpectedChunk3 + - fakeExpectedChunk4 + - fakeExpectedChunk5 - ) - val fakeText = ( - fakeExpectedChunk1 + - forge.aString(fakeExpectedChunk2.length) { forge.anAlphaNumericalChar() } + - fakeExpectedChunk3 + - forge.aString(fakeExpectedChunk4.length) { forge.anAlphaNumericalChar() } + - fakeExpectedChunk5 - ) - - // When - val obfuscatedText = testedObfuscator.obfuscate(fakeText) - - // Then - assertThat(obfuscatedText).isEqualTo(fakeExpectedText) - } - - @Test - fun `M mask String W obfuscate(){string with whitespace character}`( - forge: Forge - ) { - // Given - val fakeExpectedChunk1 = forge.aWhitespaceString() - val fakeExpectedChunk2 = forge.aString { 'x' } - val fakeExpectedChunk3 = forge.aWhitespaceString() - val fakeExpectedChunk4 = forge.aString { 'x' } - val fakeExpectedChunk5 = forge.aWhitespaceString() - val fakeExpectedText = ( - fakeExpectedChunk1 + - fakeExpectedChunk2 + - fakeExpectedChunk3 + - fakeExpectedChunk4 + - fakeExpectedChunk5 - ) - val fakeText = ( - fakeExpectedChunk1 + - forge.aString(fakeExpectedChunk2.length) { forge.anAlphaNumericalChar() } + - fakeExpectedChunk3 + - forge.aString(fakeExpectedChunk4.length) { forge.anAlphaNumericalChar() } + - fakeExpectedChunk5 - ) - - // When - val obfuscatedText = testedObfuscator.obfuscate(fakeText) - - // Then - assertThat(obfuscatedText).isEqualTo(fakeExpectedText) - } - - @Test - fun `M mask String W obfuscate(){string with whitespace character and emoji}`( - forge: Forge - ) { - // Given - val fakeChunk1 = forge.aStringWithEmoji() - val fakeChunk2 = forge.aWhitespaceString() - val fakeChunk3 = forge.aStringWithEmoji() - val fakeChunk4 = forge.aWhitespaceString() - val fakeText = fakeChunk1 + fakeChunk2 + fakeChunk3 + fakeChunk4 - - // the real size of an emoji chunk is chunk.length/2 as one emoji contains 2 chars. - // In our current code for < Android N we do not treat this case correctly so the - // expected obfuscated string size will be chunk.length - val fakeExpectedChunk1 = String(CharArray(fakeChunk1.length) { 'x' }) - val fakeExpectedChunk3 = String(CharArray(fakeChunk3.length) { 'x' }) - val fakeExpectedText = fakeExpectedChunk1 + fakeChunk2 + fakeExpectedChunk3 + fakeChunk4 - - // When - val obfuscatedText = testedObfuscator.obfuscate(fakeText) - - // Then - assertThat(obfuscatedText).isEqualTo(fakeExpectedText) - } - - // endregion - // region Internal private fun Forge.aStringWithEmoji(): String { diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/LegacyStringObfuscatorTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/LegacyStringObfuscatorTest.kt new file mode 100644 index 0000000000..3d40228566 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/LegacyStringObfuscatorTest.kt @@ -0,0 +1,175 @@ +/* + * 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.sessionreplay.internal.recorder.obfuscator + +import com.datadog.android.sessionreplay.forge.ForgeConfigurator +import com.datadog.tools.unit.extensions.ApiLevelExtension +import fr.xgouchet.elmyr.Forge +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.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import java.util.LinkedList + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(ApiLevelExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(ForgeConfigurator::class) +internal class LegacyStringObfuscatorTest { + + lateinit var testedObfuscator: StringObfuscator + + @BeforeEach + fun `set up`() { + testedObfuscator = LegacyStringObfuscator() + } + + @Test + fun `M mask String W obfuscate(){string with newline}`( + forge: Forge + ) { + // Given + val fakeExpectedChunk1 = forge.aString(size = forge.anInt(1, max = 10)) { '\n' } + val fakeExpectedChunk2 = forge.aString { 'x' } + val fakeExpectedChunk3 = forge.aString(size = forge.anInt(1, max = 10)) { '\n' } + val fakeExpectedChunk4 = forge.aString { 'x' } + val fakeExpectedChunk5 = forge.aString(size = forge.anInt(1, max = 10)) { '\n' } + val fakeExpectedText = ( + fakeExpectedChunk1 + + fakeExpectedChunk2 + + fakeExpectedChunk3 + + fakeExpectedChunk4 + + fakeExpectedChunk5 + ) + val fakeText = ( + fakeExpectedChunk1 + + forge.aString(fakeExpectedChunk2.length) { forge.anAlphaNumericalChar() } + + fakeExpectedChunk3 + + forge.aString(fakeExpectedChunk4.length) { forge.anAlphaNumericalChar() } + + fakeExpectedChunk5 + ) + + // When + val obfuscatedText = testedObfuscator.obfuscate(fakeText) + + // Then + assertThat(obfuscatedText).isEqualTo(fakeExpectedText) + } + + @Test + fun `M mask String W obfuscate(){string with carriage return character}`( + forge: Forge + ) { + // Given + val fakeExpectedChunk1 = forge.aString(size = forge.anInt(1, max = 10)) { '\r' } + val fakeExpectedChunk2 = forge.aString { 'x' } + val fakeExpectedChunk3 = forge.aString(size = forge.anInt(1, max = 10)) { '\r' } + val fakeExpectedChunk4 = forge.aString { 'x' } + val fakeExpectedChunk5 = forge.aString(size = forge.anInt(1, max = 10)) { '\r' } + val fakeExpectedText = ( + fakeExpectedChunk1 + + fakeExpectedChunk2 + + fakeExpectedChunk3 + + fakeExpectedChunk4 + + fakeExpectedChunk5 + ) + val fakeText = ( + fakeExpectedChunk1 + + forge.aString(fakeExpectedChunk2.length) { forge.anAlphaNumericalChar() } + + fakeExpectedChunk3 + + forge.aString(fakeExpectedChunk4.length) { forge.anAlphaNumericalChar() } + + fakeExpectedChunk5 + ) + + // When + val obfuscatedText = testedObfuscator.obfuscate(fakeText) + + // Then + assertThat(obfuscatedText).isEqualTo(fakeExpectedText) + } + + @Test + fun `M mask String W obfuscate(){string with whitespace character}`( + forge: Forge + ) { + // Given + val fakeExpectedChunk1 = forge.aWhitespaceString() + val fakeExpectedChunk2 = forge.aString { 'x' } + val fakeExpectedChunk3 = forge.aWhitespaceString() + val fakeExpectedChunk4 = forge.aString { 'x' } + val fakeExpectedChunk5 = forge.aWhitespaceString() + val fakeExpectedText = ( + fakeExpectedChunk1 + + fakeExpectedChunk2 + + fakeExpectedChunk3 + + fakeExpectedChunk4 + + fakeExpectedChunk5 + ) + val fakeText = ( + fakeExpectedChunk1 + + forge.aString(fakeExpectedChunk2.length) { forge.anAlphaNumericalChar() } + + fakeExpectedChunk3 + + forge.aString(fakeExpectedChunk4.length) { forge.anAlphaNumericalChar() } + + fakeExpectedChunk5 + ) + + // When + val obfuscatedText = testedObfuscator.obfuscate(fakeText) + + // Then + assertThat(obfuscatedText).isEqualTo(fakeExpectedText) + } + + @Test + fun `M mask String W obfuscate(){string with whitespace character and emoji}`( + forge: Forge + ) { + // Given + val fakeChunk1 = forge.aStringWithEmoji() + val fakeChunk2 = forge.aWhitespaceString() + val fakeChunk3 = forge.aStringWithEmoji() + val fakeChunk4 = forge.aWhitespaceString() + val fakeText = fakeChunk1 + fakeChunk2 + fakeChunk3 + fakeChunk4 + + // the real size of an emoji chunk is chunk.length/2 as one emoji contains 2 chars. + // In our current code for < Android N we do not treat this case correctly so the + // expected obfuscated string size will be chunk.length + val fakeExpectedChunk1 = String(CharArray(fakeChunk1.length) { 'x' }) + val fakeExpectedChunk3 = String(CharArray(fakeChunk3.length) { 'x' }) + val fakeExpectedText = fakeExpectedChunk1 + fakeChunk2 + fakeExpectedChunk3 + fakeChunk4 + + // When + val obfuscatedText = testedObfuscator.obfuscate(fakeText) + + // Then + assertThat(obfuscatedText).isEqualTo(fakeExpectedText) + } + + // endregion + + // region Internal + + private fun Forge.aStringWithEmoji(): String { + val charsList = LinkedList() + val stringSize = anInt(min = 10, max = 100) + repeat(stringSize) { + val emojiCodePoint = anInt(min = 0x1f600, max = 0x1f60A) + val chars = Character.toChars(emojiCodePoint).toList() + charsList.addAll(chars) + } + return String(charsList.toCharArray()) + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/rules/BaseObfuscationRuleTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/rules/BaseObfuscationRuleTest.kt index 85f556c42f..95116227a7 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/rules/BaseObfuscationRuleTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/obfuscator/rules/BaseObfuscationRuleTest.kt @@ -8,8 +8,8 @@ package com.datadog.android.sessionreplay.internal.recorder.obfuscator.rules import android.widget.TextView import com.datadog.android.sessionreplay.internal.recorder.MappingContext -import com.datadog.android.sessionreplay.internal.recorder.obfuscator.DefaultStringObfuscator import com.datadog.android.sessionreplay.internal.recorder.obfuscator.FixedLengthStringObfuscator +import com.datadog.android.sessionreplay.internal.recorder.obfuscator.LegacyStringObfuscator import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.StringForgery import org.mockito.Mock @@ -36,7 +36,7 @@ internal abstract class BaseObfuscationRuleTest { protected lateinit var mockFixedLengthStringObfuscator: FixedLengthStringObfuscator @Mock - protected lateinit var mockDefaultStringObfuscator: DefaultStringObfuscator + protected lateinit var mockDefaultStringObfuscator: LegacyStringObfuscator @StringForgery protected lateinit var fakeFixedLengthMask: String diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManagerTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManagerTest.kt new file mode 100644 index 0000000000..3f20197199 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManagerTest.kt @@ -0,0 +1,207 @@ +/* + * 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.sessionreplay.internal.recorder.resources + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import com.datadog.android.api.InternalLogger +import com.datadog.android.sessionreplay.forge.ForgeConfigurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +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.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(ForgeConfigurator::class) +internal class BitmapCachesManagerTest { + private lateinit var testedCachesManager: BitmapCachesManager + + @Mock + lateinit var mockBitmapPool: BitmapPool + + @Mock + lateinit var mockResourcesCache: ResourcesLRUCache + + @Mock + lateinit var mockLogger: InternalLogger + + @Mock + lateinit var mockApplicationContext: Context + + @Mock + lateinit var mockDrawable: Drawable + + @Mock + lateinit var mockBitmap: Bitmap + + @StringForgery + lateinit var fakeResourceId: String + + @BeforeEach + fun `set up`() { + testedCachesManager = createBitmapCachesManager( + bitmapPool = mockBitmapPool, + resourcesLRUCache = mockResourcesCache, + logger = mockLogger + ) + } + + @Test + fun `M register callbacks only once W registerCallbacks`() { + // When + repeat(times = 5) { + testedCachesManager.registerCallbacks(mockApplicationContext) + } + + // Then + verify(mockApplicationContext).registerComponentCallbacks(mockResourcesCache) + verify(mockApplicationContext).registerComponentCallbacks(mockBitmapPool) + } + + @Test + fun `M log error W registerCallbacks() { cache does not subclass ComponentCallbacks2 }`() { + // Given + val fakeBase64CacheInstance = FakeNonComponentsCallbackCache() + testedCachesManager = createBitmapCachesManager( + bitmapPool = mockBitmapPool, + resourcesLRUCache = fakeBase64CacheInstance, + logger = mockLogger + ) + + // When + testedCachesManager.registerCallbacks(mockApplicationContext) + + // Then + mockLogger.verifyLog( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + message = Cache.DOES_NOT_IMPLEMENT_COMPONENTCALLBACKS + ) + } + + @Test + fun `M put in resource cache W putInResourceCache`() { + // When + testedCachesManager.putInResourceCache(mockDrawable, fakeResourceId) + + // Then + verify(mockResourcesCache).put(mockDrawable, fakeResourceId.toByteArray(Charsets.UTF_8)) + } + + @Test + fun `M get resource from resource cache W getFromResourceCache { resource exists in cache }`() { + // Given + val fakeCacheData = fakeResourceId.toByteArray(Charsets.UTF_8) + whenever(mockResourcesCache.get(mockDrawable)).thenReturn(fakeCacheData) + + // When + val result = testedCachesManager.getFromResourceCache(mockDrawable) + + // Then + assertThat(result).isEqualTo(fakeResourceId) + } + + @Test + fun `M get null from resource cache W getFromResourceCache { resource not in cache }`() { + // When + val result = testedCachesManager.getFromResourceCache(mockDrawable) + + // Then + verify(mockResourcesCache).get(mockDrawable) + assertThat(result).isNull() + } + + @Test + fun `M put in bitmap pool W putInBitmapPool`() { + // When + testedCachesManager.putInBitmapPool(mockBitmap) + + // Then + verify(mockBitmapPool).put(mockBitmap) + } + + @Test + fun `M get bitmap by properties W getBitmapByProperties`( + @IntForgery fakeWidth: Int, + @IntForgery fakeHeight: Int, + @Mock mockConfig: Bitmap.Config + ) { + // Given + whenever( + mockBitmapPool.getBitmapByProperties( + fakeWidth, + fakeHeight, + mockConfig + ) + ).thenReturn(mockBitmap) + + // When + val result = testedCachesManager.getBitmapByProperties(fakeWidth, fakeHeight, mockConfig) + + // Then + assertThat(result).isEqualTo(mockBitmap) + } + + @Test + fun `return null W getBitmapByProperties { no bitmap found }`( + @IntForgery fakeWidth: Int, + @IntForgery fakeHeight: Int, + @Mock mockConfig: Bitmap.Config + ) { + // Given + whenever( + mockBitmapPool.getBitmapByProperties( + fakeWidth, + fakeHeight, + mockConfig + ) + ).thenReturn(null) + + // When + val result = testedCachesManager.getBitmapByProperties(fakeWidth, fakeHeight, mockConfig) + + // Then + assertThat(result).isNull() + } + + private fun createBitmapCachesManager( + bitmapPool: BitmapPool, + resourcesLRUCache: Cache, + logger: InternalLogger + ): BitmapCachesManager = + BitmapCachesManager( + bitmapPool = bitmapPool, + resourcesLRUCache = resourcesLRUCache, + logger = logger + ) + + // this is in order to test having a class that implements + // Cache, but does NOT implement ComponentCallbacks2 + private class FakeNonComponentsCallbackCache : Cache { + + override fun size(): Int = 0 + + override fun clear() {} + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageWireframeHelperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelperTest.kt similarity index 77% rename from features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageWireframeHelperTest.kt rename to features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelperTest.kt index bfd489c5ed..c868f3a671 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageWireframeHelperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelperTest.kt @@ -17,15 +17,17 @@ import android.view.View import android.widget.TextView import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.SystemInformation import com.datadog.android.sessionreplay.internal.recorder.ViewUtilsInternal -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper.Companion.APPLICATION_CONTEXT_NULL_ERROR -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper.Companion.DRAWABLE_CHILD_NAME -import com.datadog.android.sessionreplay.internal.recorder.resources.ImageWireframeHelper.Companion.RESOURCES_NULL_ERROR +import com.datadog.android.sessionreplay.internal.recorder.resources.DefaultImageWireframeHelper.Companion.APPLICATION_CONTEXT_NULL_ERROR +import com.datadog.android.sessionreplay.internal.recorder.resources.DefaultImageWireframeHelper.Companion.RESOURCES_NULL_ERROR import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.ImageWireframeHelper +import com.datadog.android.sessionreplay.utils.ImageWireframeHelper.Companion.DRAWABLE_CHILD_NAME +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import com.datadog.android.utils.isCloseTo import com.datadog.android.utils.verifyLog import fr.xgouchet.elmyr.Forge @@ -44,9 +46,7 @@ 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.atLeastOnce import org.mockito.kotlin.mock -import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions @@ -61,23 +61,17 @@ import java.util.Locale ) @MockitoSettings(strictness = Strictness.LENIENT) @ForgeConfiguration(ForgeConfigurator::class) -internal class ImageWireframeHelperTest { +internal class DefaultImageWireframeHelperTest { private lateinit var testedHelper: ImageWireframeHelper @Mock - lateinit var mockResourcesSerializer: ResourcesSerializer + lateinit var mockResourceResolver: ResourceResolver @Mock lateinit var mockLogger: InternalLogger @Mock - lateinit var mockUniqueIdentifierGenerator: UniqueIdentifierGenerator - - @Mock - lateinit var mockImageCompression: ImageCompression - - @Mock - lateinit var mockImageWireframeHelperCallback: ImageWireframeHelperCallback + lateinit var mockAsyncJobStatusCallback: AsyncJobStatusCallback @Mock lateinit var mockImageTypeResolver: ImageTypeResolver @@ -92,6 +86,9 @@ internal class ImageWireframeHelperTest { lateinit var mockTextView: TextView @Mock + lateinit var mockViewIdentifierResolver: ViewIdentifierResolver + + @Mock // TODO RUM-000 use forgery instead of mock ! lateinit var mockMappingContext: MappingContext @Mock @@ -100,7 +97,7 @@ internal class ImageWireframeHelperTest { @Mock lateinit var mockDrawable: Drawable - @Mock + @Mock // TODO RUM-000 use forgery instead of mock ! lateinit var mockBounds: GlobalBounds @Mock @@ -123,12 +120,12 @@ internal class ImageWireframeHelperTest { private lateinit var fakeDrawableXY: Pair - @StringForgery - var fakeMimeType: String = "" - @IntForgery(min = 1) var fakePadding: Int = 0 + @StringForgery + lateinit var fakeResourceId: String + @BeforeEach fun `set up`(forge: Forge) { val fakeScreenWidth = 1000 @@ -139,27 +136,26 @@ internal class ImageWireframeHelperTest { fakeDrawableXY = Pair(randomXLocation, randomYLocation) whenever(mockMappingContext.systemInformation).thenReturn(mockSystemInformation) whenever(mockSystemInformation.screenDensity).thenReturn(0f) - whenever(mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier(mockView, "drawable")) + whenever(mockViewIdentifierResolver.resolveChildUniqueIdentifier(mockView, "drawable")) .thenReturn(fakeGeneratedIdentifier) - whenever(mockDrawable.intrinsicWidth).thenReturn(fakeDrawableWidth.toInt()) - whenever(mockDrawable.intrinsicHeight).thenReturn(fakeDrawableHeight.toInt()) + whenever(mockDrawable.intrinsicWidth).thenReturn(fakeDrawableWidth) + whenever(mockDrawable.intrinsicHeight).thenReturn(fakeDrawableHeight) whenever(mockResources.displayMetrics).thenReturn(mockDisplayMetrics) whenever(mockView.resources).thenReturn(mockResources) whenever(mockView.context).thenReturn(mockContext) whenever(mockContext.applicationContext).thenReturn(mockContext) - whenever(mockImageCompression.getMimeType()).thenReturn(fakeMimeType) whenever(mockTextView.resources).thenReturn(mockResources) whenever(mockTextView.context).thenReturn(mockContext) whenever(mockViewUtilsInternal.resolveDrawableBounds(any(), any(), any())) .thenReturn(mockBounds) - whenever(mockTextView.width).thenReturn(fakeDrawableWidth.toInt()) - whenever(mockTextView.height).thenReturn(fakeDrawableHeight.toInt()) + whenever(mockTextView.width).thenReturn(fakeDrawableWidth) + whenever(mockTextView.height).thenReturn(fakeDrawableHeight) whenever(mockTextView.paddingStart).thenReturn(fakePadding) whenever(mockTextView.paddingEnd).thenReturn(fakePadding) whenever(mockTextView.paddingTop).thenReturn(fakePadding) whenever(mockTextView.paddingBottom).thenReturn(fakePadding) whenever( - mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockViewIdentifierResolver.resolveChildUniqueIdentifier( mockTextView, DRAWABLE_CHILD_NAME + 1 ) @@ -169,11 +165,10 @@ internal class ImageWireframeHelperTest { whenever(mockBounds.x).thenReturn(0L) whenever(mockBounds.y).thenReturn(0L) - testedHelper = ImageWireframeHelper( + testedHelper = DefaultImageWireframeHelper( logger = mockLogger, - resourcesSerializer = mockResourcesSerializer, - imageCompression = mockImageCompression, - uniqueIdentifierGenerator = mockUniqueIdentifierGenerator, + resourceResolver = mockResourceResolver, + viewIdentifierResolver = mockViewIdentifierResolver, viewUtilsInternal = mockViewUtilsInternal, imageTypeResolver = mockImageTypeResolver ) @@ -198,7 +193,7 @@ internal class ImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) // Then @@ -222,7 +217,7 @@ internal class ImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) // Then @@ -250,7 +245,7 @@ internal class ImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) // Then @@ -261,29 +256,10 @@ internal class ImageWireframeHelperTest { ) } - @Test - fun `M return null W createImageWireframe() { drawable is null }`() { - // When - val wireframe = testedHelper.createImageWireframe( - view = mockView, - currentWireframeIndex = 0, - x = 0, - y = 0, - width = 0, - height = 0, - usePIIPlaceholder = true, - imageWireframeHelperCallback = mockImageWireframeHelperCallback - ) - - // Then - assertThat(wireframe).isNull() - verifyNoInteractions(mockImageWireframeHelperCallback) - } - @Test fun `M return null W createImageWireframe() { id is null }`() { // Given - whenever(mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier(any(), any())) + whenever(mockViewIdentifierResolver.resolveChildUniqueIdentifier(any(), any())) .thenReturn(null) // When @@ -298,12 +274,12 @@ internal class ImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) // Then assertThat(wireframe).isNull() - verifyNoInteractions(mockImageWireframeHelperCallback) + verifyNoInteractions(mockAsyncJobStatusCallback) } @Test @@ -323,7 +299,7 @@ internal class ImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) // Then @@ -347,7 +323,7 @@ internal class ImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) // Then @@ -361,8 +337,22 @@ internal class ImageWireframeHelperTest { @Mock stubWireframeClip: MobileSegment.WireframeClip ) { // Given - whenever(mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier(any(), any())) + whenever(mockViewIdentifierResolver.resolveChildUniqueIdentifier(any(), any())) .thenReturn(fakeGeneratedIdentifier) + whenever( + mockResourceResolver.resolveResourceId( + any(), + any(), + any(), + any(), + any(), + any(), + any() + ) + ).thenAnswer { + val callback = it.arguments[6] as ResourceResolverCallback + callback.onSuccess(fakeResourceId) + } val expectedWireframe = MobileSegment.Wireframe.ImageWireframe( id = fakeGeneratedIdentifier, @@ -372,9 +362,9 @@ internal class ImageWireframeHelperTest { height = fakeDrawableHeight.toLong(), shapeStyle = mockShapeStyle, border = mockBorder, + resourceId = fakeResourceId, clip = stubWireframeClip, - mimeType = fakeMimeType, - isEmpty = true + isEmpty = false ) // When @@ -388,29 +378,24 @@ internal class ImageWireframeHelperTest { drawable = mockDrawable, shapeStyle = mockShapeStyle, border = mockBorder, - imageWireframeHelperCallback = mockImageWireframeHelperCallback, + asyncJobStatusCallback = mockAsyncJobStatusCallback, usePIIPlaceholder = true, clipping = stubWireframeClip ) // Then - val argumentCaptor = argumentCaptor() - verify(mockResourcesSerializer).handleBitmap( + verify(mockResourceResolver).resolveResourceId( resources = any(), applicationContext = any(), displayMetrics = any(), drawable = any(), drawableWidth = any(), drawableHeight = any(), - imageWireframe = any(), - resourcesSerializerCallback = argumentCaptor.capture() + resourceResolverCallback = any() ) - argumentCaptor.allValues.forEach { - it.onReady() - } - verify(mockImageWireframeHelperCallback).onStart() - verify(mockImageWireframeHelperCallback).onFinished() - verifyNoMoreInteractions(mockImageWireframeHelperCallback) + verify(mockAsyncJobStatusCallback).jobStarted() + verify(mockAsyncJobStatusCallback).jobFinished() + verifyNoMoreInteractions(mockAsyncJobStatusCallback) assertThat(wireframe).isEqualTo(expectedWireframe) } @@ -429,11 +414,11 @@ internal class ImageWireframeHelperTest { mockTextView, mockMappingContext, 0, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) // Then - verifyNoInteractions(mockImageWireframeHelperCallback) + verifyNoInteractions(mockAsyncJobStatusCallback) assertThat(wireframes).isEmpty() } @@ -457,27 +442,27 @@ internal class ImageWireframeHelperTest { mockTextView, mockMappingContext, 0, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) wireframes[0] as MobileSegment.Wireframe.ImageWireframe // Then - val argumentCaptor = argumentCaptor() - verify(mockResourcesSerializer).handleBitmap( + val argumentCaptor = argumentCaptor() + + verify(mockResourceResolver).resolveResourceId( resources = any(), applicationContext = any(), displayMetrics = any(), drawable = any(), drawableWidth = any(), drawableHeight = any(), - imageWireframe = any(), - resourcesSerializerCallback = argumentCaptor.capture() + resourceResolverCallback = argumentCaptor.capture() ) argumentCaptor.allValues.forEach { - it.onReady() + it.onSuccess(fakeResourceId) } - verify(mockImageWireframeHelperCallback).onStart() - verify(mockImageWireframeHelperCallback).onFinished() + verify(mockAsyncJobStatusCallback).jobStarted() + verify(mockAsyncJobStatusCallback).jobFinished() assertThat(wireframes.size).isEqualTo(1) } @@ -502,14 +487,13 @@ internal class ImageWireframeHelperTest { mockTextView, mockMappingContext, 0, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) wireframes[0] as MobileSegment.Wireframe.ImageWireframe // Then - val argumentCaptor = argumentCaptor() - verify(mockResourcesSerializer, times(2)).handleBitmap( - any(), + val argumentCaptor = argumentCaptor() + verify(mockResourceResolver, times(2)).resolveResourceId( any(), any(), any(), @@ -519,10 +503,10 @@ internal class ImageWireframeHelperTest { argumentCaptor.capture() ) argumentCaptor.allValues.forEach { - it.onReady() + it.onSuccess(fakeResourceId) } - verify(mockImageWireframeHelperCallback, times(2)).onStart() - verify(mockImageWireframeHelperCallback, times(2)).onFinished() + verify(mockAsyncJobStatusCallback, times(2)).jobStarted() + verify(mockAsyncJobStatusCallback, times(2)).jobFinished() assertThat(wireframes.size).isEqualTo(2) } @@ -537,11 +521,11 @@ internal class ImageWireframeHelperTest { mockTextView, mockMappingContext, 0, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) // Then - verifyNoInteractions(mockImageWireframeHelperCallback) + verifyNoInteractions(mockAsyncJobStatusCallback) assertThat(wireframes).isEmpty() } @@ -572,20 +556,19 @@ internal class ImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) // Then val captor = argumentCaptor() - verify(mockResourcesSerializer).handleBitmap( + verify(mockResourceResolver).resolveResourceId( resources = any(), applicationContext = any(), displayMetrics = any(), drawable = any(), drawableWidth = captor.capture(), drawableHeight = captor.capture(), - imageWireframe = any(), - resourcesSerializerCallback = any() + resourceResolverCallback = any() ) assertThat(captor.allValues).containsExactly(fakeViewWidth, fakeViewHeight) } @@ -604,20 +587,19 @@ internal class ImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) // Then val captor = argumentCaptor() - verify(mockResourcesSerializer).handleBitmap( + verify(mockResourceResolver).resolveResourceId( resources = any(), applicationContext = any(), displayMetrics = any(), drawable = any(), drawableWidth = captor.capture(), drawableHeight = captor.capture(), - imageWireframe = any(), - resourcesSerializerCallback = any() + resourceResolverCallback = any() ) assertThat(captor.allValues).containsExactly(fakeDrawableWidth, fakeDrawableHeight) @@ -639,8 +621,8 @@ internal class ImageWireframeHelperTest { mockDisplayMetrics.density = 1f whenever(mockContext.applicationContext).thenReturn(mockContext) val mockView: View = mock { - whenever(it.getLocationOnScreen(any())).thenAnswer { - val coords = it.arguments[0] as IntArray + whenever(it.getLocationOnScreen(any())).thenAnswer { location -> + val coords = location.arguments[0] as IntArray coords[0] = fakeGlobalX coords[1] = fakeGlobalY null @@ -662,21 +644,11 @@ internal class ImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) as MobileSegment.Wireframe.PlaceholderWireframe // Then - verify(mockResourcesSerializer, never()).handleBitmap( - resources = any(), - applicationContext = any(), - displayMetrics = any(), - drawable = any(), - drawableWidth = any(), - drawableHeight = any(), - imageWireframe = any(), - resourcesSerializerCallback = any() - ) - + verifyNoInteractions(mockResourceResolver) assertThat(isCloseTo(result.x.toInt(), fakeGlobalX)).isTrue assertThat(isCloseTo(result.y.toInt(), fakeGlobalY)).isTrue } @@ -698,19 +670,18 @@ internal class ImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) // Then - verify(mockResourcesSerializer, atLeastOnce()).handleBitmap( + verify(mockResourceResolver).resolveResourceId( resources = any(), applicationContext = any(), displayMetrics = any(), drawable = any(), drawableWidth = any(), drawableHeight = any(), - imageWireframe = any(), - resourcesSerializerCallback = any() + resourceResolverCallback = any() ) } @@ -731,13 +702,13 @@ internal class ImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, - imageWireframeHelperCallback = mockImageWireframeHelperCallback + asyncJobStatusCallback = mockAsyncJobStatusCallback ) // Then assertThat(actualWireframe).isInstanceOf(MobileSegment.Wireframe.PlaceholderWireframe::class.java) assertThat((actualWireframe as MobileSegment.Wireframe.PlaceholderWireframe).label) - .isEqualTo(ImageWireframeHelper.PLACEHOLDER_CONTENT_LABEL) + .isEqualTo(DefaultImageWireframeHelper.PLACEHOLDER_CONTENT_LABEL) } // endregion diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceItemCreationHandlerTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceItemCreationHandlerTest.kt new file mode 100644 index 0000000000..57d809a9be --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceItemCreationHandlerTest.kt @@ -0,0 +1,96 @@ +/* + * 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.sessionreplay.internal.recorder.resources + +import com.datadog.android.sessionreplay.forge.ForgeConfigurator +import com.datadog.android.sessionreplay.internal.async.DataQueueHandler +import fr.xgouchet.elmyr.annotation.StringForgery +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.verify +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(ForgeConfigurator::class) +internal class ResourceItemCreationHandlerTest { + private lateinit var testedHandler: ResourceItemCreationHandler + + @Mock + lateinit var mockDataQueueHandler: DataQueueHandler + + @StringForgery + lateinit var fakeApplicationId: String + + @StringForgery + lateinit var fakeResourceId: String + + @BeforeEach + fun `set up`() { + testedHandler = ResourceItemCreationHandler( + applicationId = fakeApplicationId, + recordedDataQueueHandler = mockDataQueueHandler + ) + } + + @Test + fun `M queue item W queueItem() { not previously seen }`() { + // Given + val fakeByteArray = fakeResourceId.toByteArray() + + // When + testedHandler.queueItem(fakeResourceId, fakeByteArray) + + // Then + verify(mockDataQueueHandler).addResourceItem( + identifier = fakeResourceId, + resourceData = fakeByteArray, + applicationId = fakeApplicationId + ) + } + + @Test + fun `M not queue item W queueItem() { previously seen }`() { + // Given + val fakeByteArray = fakeResourceId.toByteArray() + + // When + testedHandler.queueItem(fakeResourceId, fakeByteArray) + testedHandler.queueItem(fakeResourceId, fakeByteArray) + + // Then + verify(mockDataQueueHandler).addResourceItem( + identifier = fakeResourceId, + resourceData = fakeByteArray, + applicationId = fakeApplicationId + ) + } + + @Test + fun `M add unique resourceId only once W queueItem()`() { + // Given + val fakeByteArray = fakeResourceId.toByteArray() + + // When + testedHandler.queueItem(fakeResourceId, fakeByteArray) + testedHandler.queueItem(fakeResourceId, fakeByteArray) + + // Then + assertThat(testedHandler.resourceIdsSeen).hasSize(1) + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesSerializerTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverTest.kt similarity index 60% rename from features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesSerializerTest.kt rename to features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverTest.kt index 16395c6ef3..0480f64d8a 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesSerializerTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverTest.kt @@ -17,12 +17,9 @@ import android.util.DisplayMetrics import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler -import com.datadog.android.sessionreplay.internal.recorder.resources.Cache.Companion.DOES_NOT_IMPLEMENT_COMPONENTCALLBACKS import com.datadog.android.sessionreplay.internal.utils.DrawableUtils -import com.datadog.android.sessionreplay.model.MobileSegment import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery -import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension @@ -36,7 +33,6 @@ import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -56,8 +52,8 @@ import java.util.concurrent.Future ) @MockitoSettings(strictness = Strictness.LENIENT) @ForgeConfiguration(ForgeConfigurator::class) -internal class ResourcesSerializerTest { - private lateinit var testedResourcesSerializer: ResourcesSerializer +internal class ResourceResolverTest { + private lateinit var testedResourceResolver: ResourceResolver @Mock lateinit var mockDrawableUtils: DrawableUtils @@ -75,7 +71,7 @@ internal class ResourcesSerializerTest { lateinit var mockMD5HashGenerator: MD5HashGenerator @Mock - lateinit var mockSerializerCallback: ResourcesSerializerCallback + lateinit var mockSerializerCallback: ResourceResolverCallback @Mock lateinit var mockRecordedDataQueueHandler: RecordedDataQueueHandler @@ -99,7 +95,7 @@ internal class ResourcesSerializerTest { lateinit var mockStateListDrawable: StateListDrawable @Mock - lateinit var mockBitmapPool: BitmapPool + lateinit var mockBitmapCachesManager: BitmapCachesManager @Mock lateinit var mockBitmapDrawable: BitmapDrawable @@ -107,17 +103,15 @@ internal class ResourcesSerializerTest { @Mock lateinit var mockResources: Resources - @IntForgery(min = 1) - var fakeBitmapWidth: Int = 0 + private var fakeBitmapWidth: Int = 1 - @IntForgery(min = 1) - var fakeBitmapHeight: Int = 0 + private var fakeBitmapHeight: Int = 1 @Forgery lateinit var fakeApplicationid: UUID - @Forgery - lateinit var fakeImageWireframe: MobileSegment.Wireframe.ImageWireframe + @StringForgery + lateinit var fakeResourceId: String private lateinit var fakeImageCompressionByteArray: ByteArray @@ -125,7 +119,10 @@ internal class ResourcesSerializerTest { fun setup(forge: Forge) { fakeImageCompressionByteArray = forge.aString().toByteArray() - fakeImageWireframe.isEmpty = true + fakeBitmapWidth = forge.anInt(min = 1) + fakeBitmapHeight = forge.anInt(min = 1) + + whenever(mockMD5HashGenerator.generate(any())).thenReturn(fakeResourceId) whenever(mockWebPImageCompression.compressBitmap(any())) .thenReturn(fakeImageCompressionByteArray) @@ -142,10 +139,15 @@ internal class ResourcesSerializerTest { bitmapCreationCallback = any() ) ).then { - (it.arguments[7] as ResourcesSerializer.BitmapCreationCallback).onReady(mockBitmap) + (it.arguments[7] as ResourceResolver.BitmapCreationCallback).onReady(mockBitmap) } - whenever(mockExecutorService.execute(any())).then { + // executeSafe is an extension so we have to mock the internal execute function + whenever( + mockExecutorService.execute( + any() + ) + ).then { (it.arguments[0] as Runnable).run() mock>() } @@ -155,87 +157,66 @@ internal class ResourcesSerializerTest { whenever(mockBitmap.height).thenReturn(fakeBitmapHeight) whenever(mockBitmapDrawable.bitmap).thenReturn(mockBitmap) - testedResourcesSerializer = createResourcesSerializer() + testedResourceResolver = createResourceResolver() } @Test - fun `M get data from cache and update wireframe W handleBitmap() { cache hit with resourceId }`( - @StringForgery fakeResourceId: String - ) { + fun `M get data from cache W resolveResourceId() { cache hit with resourceId }`() { // Given - val fakeResourceIdByteArray = fakeResourceId.toByteArray(Charsets.UTF_8) - whenever(mockResourcesLRUCache.get(mockDrawable)).thenReturn(fakeResourceIdByteArray) + whenever(mockBitmapCachesManager.getFromResourceCache(mockDrawable)).thenReturn(fakeResourceId) whenever(mockWebPImageCompression.compressBitmap(any())) .thenReturn(fakeImageCompressionByteArray) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then verifyNoInteractions(mockDrawableUtils) - assertThat(fakeImageWireframe.isEmpty).isFalse() - assertThat(fakeImageWireframe.base64).isEqualTo(null) - assertThat(fakeImageWireframe.resourceId).isEqualTo(fakeResourceId) - verify(mockSerializerCallback).onReady() + verify(mockSerializerCallback).onSuccess(fakeResourceId) } @Test - fun `M register cache only once for callbacks W handleBitmap() { multiple calls }`() { - // When - repeat(5) { - testedResourcesSerializer.handleBitmap( - resources = mockResources, - applicationContext = mockApplicationContext, - displayMetrics = mockDisplayMetrics, - drawable = mockDrawable, - drawableWidth = mockDrawable.intrinsicWidth, - drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback - ) - } - - // Then - verify(mockApplicationContext, times(1)).registerComponentCallbacks(mockResourcesLRUCache) - } - - @Test - fun `M retry image creation only once W handleBitmap() { image was recycled while working on it }`() { + fun `M retry image creation only once W resolveResourceId() { image was recycled while working on it }`() { // Given + whenever(mockDrawableUtils.createScaledBitmap(any(), anyOrNull())) + .thenReturn(mockBitmap) + whenever(mockBitmapDrawable.bitmap).thenReturn(mockBitmap) + whenever(mockBitmap.isRecycled) - .thenReturn(true) .thenReturn(false) + .thenReturn(true) val emptyByteArray = ByteArray(0) + whenever(mockBitmapCachesManager.getFromResourceCache(mockBitmapDrawable)) + .thenReturn(null) + whenever(mockWebPImageCompression.compressBitmap(any())) .thenReturn(emptyByteArray) .thenReturn(fakeImageCompressionByteArray) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, - drawable = mockDrawable, + drawable = mockBitmapDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then - verify(mockDrawableUtils, times(2)).createBitmapOfApproxSizeFromDrawable( + verify(mockDrawableUtils).createBitmapOfApproxSizeFromDrawable( resources = any(), drawable = any(), drawableWidth = any(), @@ -248,7 +229,7 @@ internal class ResourcesSerializerTest { } @Test - fun `M send onReady W handleBitmap { failed to get image data }`() { + fun `M send onReady W resolveResourceId() { failed to get image data }`() { // Given whenever(mockBitmap.isRecycled) .thenReturn(true) @@ -260,150 +241,125 @@ internal class ResourcesSerializerTest { .thenReturn(emptyByteArray) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then - verify(mockSerializerCallback).onReady() + verify(mockSerializerCallback).onFailure() } @Test - fun `M log error W handleBitmap() { cache does not subclass ComponentCallbacks2 }`() { + fun `M calculate resourceId W resolveResourceId() { cache miss }`() { // Given - val fakeBase64CacheInstance = FakeNonComponentsCallbackCache() - testedResourcesSerializer = ResourcesSerializer.Builder( - logger = mockLogger, - threadPoolExecutor = mockExecutorService, - bitmapPool = mockBitmapPool, - resourcesLRUCache = fakeBase64CacheInstance, - drawableUtils = mockDrawableUtils, - recordedDataQueueHandler = mockRecordedDataQueueHandler, - applicationId = fakeApplicationid.toString(), - webPImageCompression = mockWebPImageCompression - ).build() + whenever(mockResourcesLRUCache.get(mockDrawable)).thenReturn(null) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then - val captor = argumentCaptor<() -> String>() - verify(mockLogger).log( - level = any(), - target = any(), - captor.capture(), - anyOrNull(), - anyOrNull(), - anyOrNull() - ) - assertThat(captor.firstValue.invoke()).isEqualTo( - DOES_NOT_IMPLEMENT_COMPONENTCALLBACKS + verify(mockDrawableUtils).createBitmapOfApproxSizeFromDrawable( + resources = any(), + drawable = any(), + drawableWidth = any(), + drawableHeight = any(), + displayMetrics = any(), + requestedSizeInBytes = anyOrNull(), + config = anyOrNull(), + bitmapCreationCallback = any() ) } @Test - fun `M register BitmapPool only once for callbacks W handleBitmap() { multiple calls }`() { - // When - repeat(5) { - testedResourcesSerializer.handleBitmap( - resources = mockResources, - applicationContext = mockApplicationContext, - displayMetrics = mockDisplayMetrics, - drawable = mockDrawable, - drawableWidth = mockDrawable.intrinsicWidth, - drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback - ) - } - - // Then - verify(mockApplicationContext, times(1)).registerComponentCallbacks(mockBitmapPool) - } - - @Test - fun `M calculate resourceId W handleBitmap() { cache miss }`() { + fun `M return failure W resolveResourceId { createBitmapOfApproxSizeFromDrawable failed }`() { // Given whenever(mockResourcesLRUCache.get(mockDrawable)).thenReturn(null) + whenever( + mockDrawableUtils.createBitmapOfApproxSizeFromDrawable( + resources = any(), + drawable = any(), + drawableWidth = any(), + drawableHeight = any(), + displayMetrics = any(), + requestedSizeInBytes = anyOrNull(), + config = anyOrNull(), + bitmapCreationCallback = any() + ) + ).then { + (it.arguments[7] as ResourceResolver.BitmapCreationCallback).onFailure() + } // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then - verify(mockDrawableUtils).createBitmapOfApproxSizeFromDrawable( - resources = any(), - drawable = any(), - drawableWidth = any(), - drawableHeight = any(), - displayMetrics = any(), - requestedSizeInBytes = anyOrNull(), - config = anyOrNull(), - bitmapCreationCallback = any() - ) + verify(mockSerializerCallback).onFailure() } @Test fun `M use the same ThreadPoolExecutor W build()`() { // When - val instance1 = ResourcesSerializer.Builder( - bitmapPool = mockBitmapPool, - resourcesLRUCache = mockResourcesLRUCache, + val instance1 = ResourceResolver( recordedDataQueueHandler = mockRecordedDataQueueHandler, - applicationId = fakeApplicationid.toString() - ).build() - val instance2 = ResourcesSerializer.Builder( - bitmapPool = mockBitmapPool, - resourcesLRUCache = mockResourcesLRUCache, + applicationId = fakeApplicationid.toString(), + webPImageCompression = mockWebPImageCompression, + drawableUtils = mockDrawableUtils, + logger = mockLogger, + md5HashGenerator = mockMD5HashGenerator, + bitmapCachesManager = mockBitmapCachesManager + ) + val instance2 = ResourceResolver( recordedDataQueueHandler = mockRecordedDataQueueHandler, - applicationId = fakeApplicationid.toString() - ).build() + applicationId = fakeApplicationid.toString(), + webPImageCompression = mockWebPImageCompression, + drawableUtils = mockDrawableUtils, + logger = mockLogger, + md5HashGenerator = mockMD5HashGenerator, + bitmapCachesManager = mockBitmapCachesManager + ) // Then - assertThat(instance1.getThreadPoolExecutor()).isEqualTo( - instance2.getThreadPoolExecutor() + assertThat(instance1.threadPoolExecutor).isEqualTo( + instance2.threadPoolExecutor ) } @Test - fun `M not try to cache resourceId W handleBitmap() { and did not get resourceId }`() { + fun `M not try to cache resourceId W resolveResourceId() { and did not get resourceId }`() { // Given whenever(mockMD5HashGenerator.generate(any())).thenReturn(null) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockStateListDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then @@ -411,20 +367,19 @@ internal class ResourcesSerializerTest { } @Test - fun `M not use bitmap from bitmapDrawable W handleBitmap() { no bitmap }`() { + fun `M not use bitmap from bitmapDrawable W resolveResourceId() { no bitmap }`() { // Given whenever(mockBitmapDrawable.bitmap).thenReturn(null) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then @@ -441,20 +396,19 @@ internal class ResourcesSerializerTest { } @Test - fun `M not use bitmap from bitmapDrawable W handleBitmap() { bitmap was recycled }`() { + fun `M not use bitmap from bitmapDrawable W resolveResourceId() { bitmap was recycled }`() { // Given whenever(mockBitmap.isRecycled).thenReturn(true) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then @@ -471,17 +425,16 @@ internal class ResourcesSerializerTest { } @Test - fun `M use scaled bitmap from bitmapDrawable W handleBitmap() { has bitmap }`() { + fun `M use scaled bitmap from bitmapDrawable W resolveResourceId() { has bitmap }`() { // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then @@ -492,20 +445,19 @@ internal class ResourcesSerializerTest { } @Test - fun `M draw bitmap W handleBitmap() { bitmapDrawable where bitmap has no width }`() { + fun `M draw bitmap W resolveResourceId() { bitmapDrawable where bitmap has no width }`() { // Given whenever(mockBitmap.width).thenReturn(0) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then @@ -526,20 +478,19 @@ internal class ResourcesSerializerTest { } @Test - fun `M draw bitmap W handleBitmap() { bitmapDrawable where bitmap has no height }`() { + fun `M draw bitmap W resolveResourceId() { bitmapDrawable where bitmap has no height }`() { // Given whenever(mockBitmap.height).thenReturn(0) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then @@ -560,124 +511,168 @@ internal class ResourcesSerializerTest { } @Test - fun `M not cache bitmap W handleBitmap() { BitmapDrawable with bitmap not resized }`() { + fun `M not cache bitmap W resolveResourceId() { BitmapDrawable with bitmap not resized }`() { // Given whenever(mockDrawableUtils.createScaledBitmap(any(), anyOrNull())) .thenReturn(mockBitmap) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then - verify(mockBitmapPool, never()).put(any()) + verify(mockBitmapCachesManager, never()).putInBitmapPool(any()) } @Test - fun `M cache bitmap W handleBitmap() { BitmapDrawable with bitmap was resized }`( - @Mock mockResizedBitmap: Bitmap + fun `M cache bitmap W resolveResourceId() { BitmapDrawable width was resized }`( + @Mock mockResizedBitmap: Bitmap, + @StringForgery fakeString: String ) { // Given - whenever(mockDrawableUtils.createScaledBitmap(any(), anyOrNull())) - .thenReturn(mockResizedBitmap) + val fakeByteArray = fakeString.toByteArray() + assertThat(fakeByteArray).isNotEmpty() + + whenever(mockBitmap.isRecycled).thenReturn(false) + whenever(mockResizedBitmap.width).thenReturn(fakeBitmapWidth - 1) + whenever(mockResizedBitmap.height).thenReturn(fakeBitmapHeight) + + whenever(mockWebPImageCompression.compressBitmap(mockResizedBitmap)).thenReturn(fakeByteArray) + whenever(mockDrawableUtils.createScaledBitmap(any(), anyOrNull())).thenReturn(mockResizedBitmap) + + // When + testedResourceResolver.resolveResourceId( + resources = mockResources, + applicationContext = mockApplicationContext, + displayMetrics = mockDisplayMetrics, + drawable = mockBitmapDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, + resourceResolverCallback = mockSerializerCallback + ) + + // Then + verify(mockBitmapCachesManager).putInBitmapPool(any()) + } + + @Test + fun `M cache bitmap W resolveResourceId() { BitmapDrawable height was resized }`( + @Mock mockResizedBitmap: Bitmap, + @StringForgery fakeString: String + ) { + // Given + val fakeByteArray = fakeString.toByteArray() + assertThat(fakeByteArray).isNotEmpty() + + whenever(mockBitmap.isRecycled).thenReturn(false) + whenever(mockResizedBitmap.width).thenReturn(fakeBitmapWidth) + whenever(mockResizedBitmap.height).thenReturn(fakeBitmapHeight - 1) + + whenever(mockWebPImageCompression.compressBitmap(mockResizedBitmap)).thenReturn(fakeByteArray) + whenever(mockDrawableUtils.createScaledBitmap(any(), anyOrNull())).thenReturn(mockResizedBitmap) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then - verify(mockBitmapPool).put(any()) + verify(mockBitmapCachesManager).putInBitmapPool(any()) } @Test - fun `M cache bitmap W handleBitmap() { from BitmapDrawable with null bitmap }`() { + fun `M cache bitmap W resolveResourceId() { from BitmapDrawable with null bitmap }`() { // Given + whenever(mockBitmapCachesManager.getFromResourceCache(mockBitmapDrawable)) + .thenReturn(null) whenever(mockBitmapDrawable.bitmap).thenReturn(null) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then - verify(mockBitmapPool, times(1)).put(any()) + verify(mockBitmapCachesManager, times(1)).putInBitmapPool(any()) } @Test - fun `M cache bitmap W handleBitmap() { not a BitmapDrawable }`() { + fun `M cache bitmap W resolveResourceId() { not a BitmapDrawable }`() { // Given val mockLayerDrawable = mock() // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockLayerDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) // Then - verify(mockBitmapPool, times(1)).put(any()) + verify(mockBitmapCachesManager, times(1)).putInBitmapPool(any()) } @Test - fun `M return correct callback W handleBitmap() { multiple threads, first takes longer }`( - @Mock mockFirstCallback: ResourcesSerializerCallback, - @Mock mockSecondCallback: ResourcesSerializerCallback + fun `M return all callbacks W resolveResourceId() { multiple threads, first takes longer }`( + @Mock mockFirstCallback: ResourceResolverCallback, + @Mock mockSecondCallback: ResourceResolverCallback, + @Mock mockFirstDrawable: Drawable, + @Mock mockSecondDrawable: Drawable, + @StringForgery fakeFirstResourceId: String, + @StringForgery fakeSecondResourceId: String ) { // Given + whenever(mockBitmapCachesManager.getFromResourceCache(mockFirstDrawable)) + .thenReturn(fakeFirstResourceId) + whenever(mockBitmapCachesManager.getFromResourceCache(mockSecondDrawable)) + .thenReturn(fakeSecondResourceId) + val countDownLatch = CountDownLatch(2) val thread1 = Thread { - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, - drawable = mockDrawable, + drawable = mockFirstDrawable, drawableWidth = fakeBitmapWidth, drawableHeight = fakeBitmapHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockFirstCallback + resourceResolverCallback = mockFirstCallback ) Thread.sleep(1500) countDownLatch.countDown() } val thread2 = Thread { - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, - drawable = mockDrawable, + drawable = mockSecondDrawable, drawableWidth = fakeBitmapWidth, drawableHeight = fakeBitmapHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSecondCallback + resourceResolverCallback = mockSecondCallback ) Thread.sleep(500) countDownLatch.countDown() @@ -689,12 +684,12 @@ internal class ResourcesSerializerTest { // Then countDownLatch.await() - verify(mockFirstCallback).onReady() - verify(mockSecondCallback).onReady() + verify(mockFirstCallback).onSuccess(fakeFirstResourceId) + verify(mockSecondCallback).onSuccess(fakeSecondResourceId) } @Test - fun `M failover to bitmap creation W handleBitmap { bitmapDrawable returned empty bytearray }`( + fun `M failover to bitmap creation W resolveResourceId() { bitmapDrawable returned empty bytearray }`( @Mock mockCreatedBitmap: Bitmap ) { // Given @@ -718,46 +713,31 @@ internal class ResourcesSerializerTest { .thenReturn(mockCreatedBitmap) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, drawableWidth = fakeBitmapWidth, drawableHeight = fakeBitmapHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) - val drawableCaptor = argumentCaptor() - val intCaptor = argumentCaptor() - val displayMetricsCaptor = argumentCaptor() - val configCaptor = argumentCaptor() - val bitmapCreationCallbackCaptor = argumentCaptor() - val resourcesCaptor = argumentCaptor() - // Then - verify(mockDrawableUtils, times(1)).createBitmapOfApproxSizeFromDrawable( - resources = resourcesCaptor.capture(), - drawable = drawableCaptor.capture(), - drawableWidth = intCaptor.capture(), - drawableHeight = intCaptor.capture(), - displayMetrics = displayMetricsCaptor.capture(), - requestedSizeInBytes = intCaptor.capture(), - config = configCaptor.capture(), - bitmapCreationCallback = bitmapCreationCallbackCaptor.capture() - ) - - assertThat(drawableCaptor.firstValue).isEqualTo(mockBitmapDrawable) - assertThat(intCaptor.firstValue).isEqualTo(fakeBitmapWidth) - assertThat(intCaptor.secondValue).isEqualTo(fakeBitmapHeight) - assertThat(displayMetricsCaptor.firstValue).isEqualTo(mockDisplayMetrics) - assertThat(configCaptor.firstValue).isEqualTo(Bitmap.Config.ARGB_8888) - assertThat(resourcesCaptor.firstValue).isEqualTo(mockResources) + verify(mockDrawableUtils).createBitmapOfApproxSizeFromDrawable( + resources = any(), + drawable = any(), + drawableWidth = any(), + drawableHeight = any(), + displayMetrics = any(), + requestedSizeInBytes = anyOrNull(), + config = anyOrNull(), + bitmapCreationCallback = any() + ) } @Test - fun `M only send resource once W handleBitmap { call twice on the same image }`( + fun `M only send resource once W resolveResourceId() { call twice on the same image }`( @Mock mockCreatedBitmap: Bitmap, @StringForgery fakeResourceId: String, @StringForgery fakeResource: String @@ -784,15 +764,27 @@ internal class ResourcesSerializerTest { .thenReturn(mockCreatedBitmap) // When - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( + resources = mockResources, + applicationContext = mockApplicationContext, + displayMetrics = mockDisplayMetrics, + drawable = mockBitmapDrawable, + drawableWidth = fakeBitmapWidth, + drawableHeight = fakeBitmapHeight, + resourceResolverCallback = mockSerializerCallback + ) + + // Then + + // second time + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, drawableWidth = fakeBitmapWidth, drawableHeight = fakeBitmapHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) verify(mockRecordedDataQueueHandler, times(1)).addResourceItem( @@ -802,45 +794,31 @@ internal class ResourcesSerializerTest { ) // second time - testedResourcesSerializer.handleBitmap( + testedResourceResolver.resolveResourceId( resources = mockResources, applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, drawableWidth = fakeBitmapWidth, drawableHeight = fakeBitmapHeight, - imageWireframe = fakeImageWireframe, - resourcesSerializerCallback = mockSerializerCallback + resourceResolverCallback = mockSerializerCallback ) - verify(mockRecordedDataQueueHandler, times(1)).addResourceItem( - identifier = eq(fakeResourceId), - applicationId = eq(fakeApplicationid.toString()), - resourceData = eq(fakeByteArray) - ) - } - - private fun createResourcesSerializer(): ResourcesSerializer { - val builder = ResourcesSerializer.Builder( - logger = mockLogger, - threadPoolExecutor = mockExecutorService, - bitmapPool = mockBitmapPool, - resourcesLRUCache = mockResourcesLRUCache, - drawableUtils = mockDrawableUtils, - webPImageCompression = mockWebPImageCompression, - md5HashGenerator = mockMD5HashGenerator, - recordedDataQueueHandler = mockRecordedDataQueueHandler, - applicationId = fakeApplicationid.toString() + verify(mockRecordedDataQueueHandler, times(1)).addResourceItem( + identifier = eq(fakeResourceId), + applicationId = eq(fakeApplicationid.toString()), + resourceData = eq(fakeByteArray) ) - return builder.build() } - // this is in order to test having a class that implements - // Cache, but does NOT implement ComponentCallbacks2 - private class FakeNonComponentsCallbackCache : Cache { - - override fun size(): Int = 0 - - override fun clear() {} - } + private fun createResourceResolver(): ResourceResolver = ResourceResolver( + logger = mockLogger, + threadPoolExecutor = mockExecutorService, + drawableUtils = mockDrawableUtils, + webPImageCompression = mockWebPImageCompression, + md5HashGenerator = mockMD5HashGenerator, + recordedDataQueueHandler = mockRecordedDataQueueHandler, + applicationId = fakeApplicationid.toString(), + bitmapCachesManager = mockBitmapCachesManager + ) } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/WebPImageCompressionTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/WebPImageCompressionTest.kt index 3b84eccbb9..811bcd399e 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/WebPImageCompressionTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/WebPImageCompressionTest.kt @@ -8,6 +8,7 @@ package com.datadog.android.sessionreplay.internal.recorder.resources import android.graphics.Bitmap import android.os.Build +import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.tools.unit.annotations.TestTargetApi import com.datadog.tools.unit.extensions.ApiLevelExtension @@ -40,9 +41,12 @@ internal class WebPImageCompressionTest { @Mock lateinit var mockBitmap: Bitmap + @Mock + lateinit var logger: InternalLogger + @BeforeEach fun setup() { - testedImageCompression = WebPImageCompression() + testedImageCompression = WebPImageCompression(logger) } // region compressBitmapToStream diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtilsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtilsTest.kt index 83cd9f8db9..dfe95a707f 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtilsTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtilsTest.kt @@ -15,8 +15,8 @@ import android.os.Handler import android.util.DisplayMetrics import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.resources.BitmapPool -import com.datadog.android.sessionreplay.internal.recorder.resources.ResourcesSerializer +import com.datadog.android.sessionreplay.internal.recorder.resources.BitmapCachesManager +import com.datadog.android.sessionreplay.internal.recorder.resources.ResourceResolver import com.datadog.android.sessionreplay.internal.recorder.wrappers.BitmapWrapper import com.datadog.android.sessionreplay.internal.recorder.wrappers.CanvasWrapper import fr.xgouchet.elmyr.annotation.IntForgery @@ -54,7 +54,7 @@ internal class DrawableUtilsTest { private lateinit var mockDisplayMetrics: DisplayMetrics @Mock - private lateinit var mockBitmapPool: BitmapPool + private lateinit var mockBitmapCachesManager: BitmapCachesManager @Mock private lateinit var mockDrawable: Drawable @@ -78,14 +78,11 @@ internal class DrawableUtilsTest { private lateinit var mockExecutorService: ExecutorService @Mock - private lateinit var mockBitmapCreationCallback: ResourcesSerializer.BitmapCreationCallback + private lateinit var mockBitmapCreationCallback: ResourceResolver.BitmapCreationCallback @Mock private lateinit var mockMainThreadHandler: Handler - @Mock - private lateinit var mockLogger: InternalLogger - @Mock lateinit var mockConstantState: ConstantState @@ -95,6 +92,9 @@ internal class DrawableUtilsTest { @Mock lateinit var mockResources: Resources + @Mock + private lateinit var mockLogger: InternalLogger + @BeforeEach fun setup() { whenever(mockConstantState.newDrawable(mockResources)).thenReturn(mockSecondDrawable) @@ -104,7 +104,7 @@ internal class DrawableUtilsTest { whenever(mockCanvasWrapper.createCanvas(any())) .thenReturn(mockCanvas) whenever(mockBitmap.config).thenReturn(mockConfig) - whenever(mockBitmapPool.getBitmapByProperties(any(), any(), any())).thenReturn(null) + whenever(mockBitmapCachesManager.getBitmapByProperties(any(), any(), any())).thenReturn(null) doAnswer { invocation -> val work = invocation.getArgument(0) as Runnable @@ -122,8 +122,7 @@ internal class DrawableUtilsTest { testedDrawableUtils = DrawableUtils( bitmapWrapper = mockBitmapWrapper, canvasWrapper = mockCanvasWrapper, - bitmapPool = mockBitmapPool, - threadPoolExecutor = mockExecutorService, + bitmapCachesManager = mockBitmapCachesManager, mainThreadHandler = mockMainThreadHandler, logger = mockLogger ) @@ -252,7 +251,7 @@ internal class DrawableUtilsTest { val mockBitmapFromPool: Bitmap = mock() whenever(mockDrawable.intrinsicWidth).thenReturn(viewWidth) whenever(mockDrawable.intrinsicHeight).thenReturn(viewHeight) - whenever(mockBitmapPool.getBitmapByProperties(any(), any(), any())) + whenever(mockBitmapCachesManager.getBitmapByProperties(any(), any(), any())) .thenReturn(mockBitmapFromPool) // When @@ -275,7 +274,7 @@ internal class DrawableUtilsTest { // Given whenever(mockDrawable.intrinsicWidth).thenReturn(1) whenever(mockDrawable.intrinsicHeight).thenReturn(1) - whenever(mockBitmapPool.getBitmapByProperties(any(), any(), any())) + whenever(mockBitmapCachesManager.getBitmapByProperties(any(), any(), any())) .thenReturn(null) whenever(mockBitmapWrapper.createBitmap(any(), any(), any(), any())) .thenReturn(null) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtilsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtilsTest.kt index 005ab65f33..f359b7bbfb 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtilsTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtilsTest.kt @@ -20,11 +20,11 @@ import android.view.WindowManager import android.view.WindowMetrics import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds import com.datadog.android.sessionreplay.internal.recorder.SystemInformation import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.internal.utils.MiscUtils.DESERIALIZE_JSON_ERROR -import com.datadog.android.sessionreplay.utils.StringUtils +import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter +import com.datadog.android.sessionreplay.utils.GlobalBounds import com.datadog.android.sessionreplay.utils.verifyLog import com.datadog.tools.unit.annotations.TestTargetApi import com.datadog.tools.unit.extensions.ApiLevelExtension @@ -118,7 +118,7 @@ internal class MiscUtilsTest { @Test fun `M resolve system information W resolveSystemInformation`(forge: Forge) { // Given - val expectedThemeColorAsHexa = StringUtils.formatColorAndAlphaAsHexa( + val expectedThemeColorAsHexa = DefaultColorStringFormatter.formatColorAndAlphaAsHexString( fakeThemeColor, MiscUtils.OPAQUE_ALPHA_VALUE ) @@ -157,7 +157,7 @@ internal class MiscUtilsTest { @Test fun `M resolve system information W resolveSystemInformation{ R and above }`(forge: Forge) { // Given - val expectedThemeColorAsHexa = StringUtils.formatColorAndAlphaAsHexa( + val expectedThemeColorAsHexa = DefaultColorStringFormatter.formatColorAndAlphaAsHexString( fakeThemeColor, MiscUtils.OPAQUE_ALPHA_VALUE ) @@ -200,7 +200,7 @@ internal class MiscUtilsTest { @Test fun `M return empty screen bounds W resolveSystemInformation{windowManager was null}`() { // Given - val expectedThemeColorAsHexa = StringUtils.formatColorAndAlphaAsHexa( + val expectedThemeColorAsHexa = DefaultColorStringFormatter.formatColorAndAlphaAsHexString( fakeThemeColor, MiscUtils.OPAQUE_ALPHA_VALUE ) @@ -285,6 +285,7 @@ internal class MiscUtilsTest { return fakeColor } + private fun Forge.forgeThemeInvalidNoColor(theme: Theme): Int { val fakeColor = aPositiveInt() whenever( diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/AndroidMDrawableToColorMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/AndroidMDrawableToColorMapperTest.kt new file mode 100644 index 0000000000..089e1121ac --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/AndroidMDrawableToColorMapperTest.kt @@ -0,0 +1,92 @@ +package com.datadog.android.sessionreplay.utils + +//noinspection SuspiciousImport +import android.R +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.InsetDrawable +import android.graphics.drawable.RippleDrawable +import android.os.Build +import com.datadog.android.sessionreplay.forge.ForgeConfigurator +import com.datadog.tools.unit.annotations.TestTargetApi +import com.datadog.tools.unit.extensions.ApiLevelExtension +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.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +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(value = ForgeConfigurator::class, seed = 0x3f3a03ceae05aL) +open class AndroidMDrawableToColorMapperTest : LegacyDrawableToColorMapperTest() { + + override fun createTestedMapper(): DrawableToColorMapper { + return AndroidMDrawableToColorMapper() + } + + @Test + @TestTargetApi(Build.VERSION_CODES.M) + fun `M map RippleDrawable to first opaque layer's color ignoring mask W mapDrawableToColor()`( + @IntForgery drawableColor: Int, + @IntForgery maskDrawableColor: Int + ) { + // Given + val baseColor = drawableColor and 0xFFFFFF + val baseAlpha = (drawableColor.toLong() and 0xFF000000) shr 24 + val maskLayer = mock().apply { + whenever(this.color) doReturn (maskDrawableColor and 0xFFFFFF) + whenever(this.alpha) doReturn baseAlpha.toInt() + } + val opaqueLayer = mock().apply { + whenever(this.color) doReturn baseColor + whenever(this.alpha) doReturn baseAlpha.toInt() + } + val rippleDrawable = mock().apply { + whenever(this.numberOfLayers) doReturn 2 + whenever(this.findIndexByLayerId(R.id.mask)) doReturn 0 + whenever(this.getDrawable(0)) doReturn maskLayer + whenever(this.getDrawable(1)) doReturn opaqueLayer + } + + // When + val result = testedMapper.mapDrawableToColor(rippleDrawable) + + // Then + assertThat(result).isEqualTo(drawableColor) + } + + @Test + @TestTargetApi(Build.VERSION_CODES.M) + fun `M map InsetDrawable to inner drawable's color W mapDrawableToColor()`( + @IntForgery drawableColor: Int + ) { + // Given + val baseColor = drawableColor and 0xFFFFFF + val baseAlpha = (drawableColor.toLong() and 0xFF000000) shr 24 + val innerDrawable = mock().apply { + whenever(this.color) doReturn baseColor + whenever(this.alpha) doReturn baseAlpha.toInt() + } + val insetDrawable = mock().apply { + whenever(this.drawable) doReturn innerDrawable + } + + // When + val result = testedMapper.mapDrawableToColor(insetDrawable) + + // Then + assertThat(result).isEqualTo(drawableColor) + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/AndroidQDrawableToColorMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/AndroidQDrawableToColorMapperTest.kt new file mode 100644 index 0000000000..b318e70776 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/AndroidQDrawableToColorMapperTest.kt @@ -0,0 +1,103 @@ +package com.datadog.android.sessionreplay.utils + +//noinspection SuspiciousImport +import android.graphics.BlendModeColorFilter +import android.graphics.Paint +import android.graphics.drawable.GradientDrawable +import android.os.Build +import com.datadog.android.sessionreplay.forge.ForgeConfigurator +import com.datadog.tools.unit.annotations.TestTargetApi +import com.datadog.tools.unit.extensions.ApiLevelExtension +import fr.xgouchet.elmyr.Forge +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.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +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(value = ForgeConfigurator::class, seed = 0x3f3a03ceae05aL) +class AndroidQDrawableToColorMapperTest : AndroidMDrawableToColorMapperTest() { + + override fun createTestedMapper(): DrawableToColorMapper { + return AndroidQDrawableToColorMapper() + } + + @Test + @TestTargetApi(Build.VERSION_CODES.Q) + fun `M map GradientDrawable to fill paint's color blend color W mapDrawableToColor()`( + @IntForgery fillPaintColor: Int, + @IntForgery blendFilterColor: Int, + forge: Forge + ) { + // Given + val blendColor = blendFilterColor and 0xFFFFFF + val expectedBlendColor = ((fillPaintColor.toLong() and 0xFF000000) or blendColor.toLong()).toInt() + val blendMode = forge.anElementFrom(AndroidQDrawableToColorMapper.blendModesReturningBlendColor) + val mockColorFilter = mock().apply { + whenever(this.color) doReturn blendColor + whenever(this.mode) doReturn blendMode + } + val baseColor = fillPaintColor and 0xFFFFFF + val baseAlpha = (fillPaintColor.toLong() and 0xFF000000) shr 24 + val mockFillPaint = mock().apply { + whenever(this.color) doReturn baseColor + whenever(this.alpha) doReturn baseAlpha.toInt() + whenever(this.colorFilter) doReturn mockColorFilter + } + val gradientDrawable = GradientDrawable().apply { + LegacyDrawableToColorMapper.fillPaintField?.set(this, mockFillPaint) + } + + // When + val result = testedMapper.mapDrawableToColor(gradientDrawable) + + // Then + assertThat(result).isEqualTo(expectedBlendColor) + } + + @Test + @TestTargetApi(Build.VERSION_CODES.Q) + fun `M map GradientDrawable to fill paint's color W mapDrawableToColor()`( + @IntForgery fillPaintColor: Int, + @IntForgery blendFilterColor: Int, + forge: Forge + ) { + // Given + val blendColor = blendFilterColor and 0xFFFFFF + val blendMode = forge.anElementFrom(AndroidQDrawableToColorMapper.blendModesReturningOriginalColor) + val mockColorFilter = mock().apply { + whenever(this.color) doReturn blendColor + whenever(this.mode) doReturn blendMode + } + val baseColor = fillPaintColor and 0xFFFFFF + val baseAlpha = (fillPaintColor.toLong() and 0xFF000000) shr 24 + val mockFillPaint = mock().apply { + whenever(this.color) doReturn baseColor + whenever(this.alpha) doReturn baseAlpha.toInt() + whenever(this.colorFilter) doReturn mockColorFilter + } + val gradientDrawable = GradientDrawable().apply { + LegacyDrawableToColorMapper.fillPaintField?.set(this, mockFillPaint) + } + + // When + val result = testedMapper.mapDrawableToColor(gradientDrawable) + + // Then + assertThat(result).isEqualTo(fillPaintColor) + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultColorStringFormatterTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultColorStringFormatterTest.kt new file mode 100644 index 0000000000..70c1f31381 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultColorStringFormatterTest.kt @@ -0,0 +1,52 @@ +/* + * 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.sessionreplay.utils + +import com.datadog.android.sessionreplay.forge.ForgeConfigurator +import fr.xgouchet.elmyr.annotation.StringForgery +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 +import org.junit.jupiter.api.extension.Extensions + +@Extensions(ExtendWith(ForgeExtension::class)) +@ForgeConfiguration(ForgeConfigurator::class) +internal class DefaultColorStringFormatterTest { + + @Test + fun `M return hex formatted color W formatColorAndAlphaAsHexString`( + @StringForgery(regex = "#[0-9a-f]{8}") fakeHexColor: String + ) { + // Given + val color = fakeHexColor.substring(1 until 7).toInt(16) + val colorAlpha = fakeHexColor.substring(7..8).toInt(16) + + // When + val hexString = DefaultColorStringFormatter.formatColorAndAlphaAsHexString(color, colorAlpha) + + // Then + assertThat(hexString).isEqualTo(fakeHexColor) + } + + @Test + fun `M return hex formatted color with original alpha W formatColorAsHexString`( + @StringForgery(regex = "#[0-9a-f]{8}") fakeHexColor: String + ) { + // Given + val color = fakeHexColor.substring(1 until 7).toInt(16) + val colorAlpha = fakeHexColor.substring(7..8).toInt(16) + val colorWithAlpha = color or (colorAlpha shl 24) + + // When + val hexString = DefaultColorStringFormatter.formatColorAsHexString(colorWithAlpha) + + // Then + assertThat(hexString).isEqualTo(fakeHexColor) + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/ViewUtilsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewBoundsResolverTest.kt similarity index 88% rename from features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/ViewUtilsTest.kt rename to features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewBoundsResolverTest.kt index c00fb02d4d..4242325e1b 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/ViewUtilsTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewBoundsResolverTest.kt @@ -29,7 +29,7 @@ import org.mockito.quality.Strictness ) @MockitoSettings(strictness = Strictness.LENIENT) @ForgeConfiguration(ForgeConfigurator::class) -internal class ViewUtilsTest { +internal class DefaultViewBoundsResolverTest { @Test fun `M correctly resolve the View global bounds W resolveViewGlobalBounds`(forge: Forge) { @@ -51,16 +51,16 @@ internal class ViewUtilsTest { val fakePixelDensity = forge.aPositiveFloat() // When - val globalBounds = ViewUtils.resolveViewGlobalBounds(mockView, fakePixelDensity) + val viewBounds = DefaultViewBoundsResolver.resolveViewGlobalBounds(mockView, fakePixelDensity) // Then - assertThat(globalBounds.x) + assertThat(viewBounds.x) .isEqualTo(fakeGlobalX.densityNormalized(fakePixelDensity).toLong()) - assertThat(globalBounds.y) + assertThat(viewBounds.y) .isEqualTo(fakeGlobalY.densityNormalized(fakePixelDensity).toLong()) - assertThat(globalBounds.width) + assertThat(viewBounds.width) .isEqualTo(fakeWidth.densityNormalized(fakePixelDensity).toLong()) - assertThat(globalBounds.height) + assertThat(viewBounds.height) .isEqualTo(fakeHeight.densityNormalized(fakePixelDensity).toLong()) } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/UniqueIdentifierGeneratorTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewIdentifierResolverTest.kt similarity index 82% rename from features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/UniqueIdentifierGeneratorTest.kt rename to features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewIdentifierResolverTest.kt index 1c1aab5e36..8772272a21 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/UniqueIdentifierGeneratorTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewIdentifierResolverTest.kt @@ -27,7 +27,7 @@ import org.mockito.quality.Strictness ) @MockitoSettings(strictness = Strictness.LENIENT) @ForgeConfiguration(ForgeConfigurator::class) -internal class UniqueIdentifierGeneratorTest { +internal class DefaultViewIdentifierResolverTest { @Test fun `M resolve an unique identifier W resolveChildUniqueIdentifier()`(forge: Forge) { @@ -37,7 +37,7 @@ internal class UniqueIdentifierGeneratorTest { // When val uniqueNumbers = forge.aList(numberOfCalls) { aString() } .map { mock() to it } - .map { UniqueIdentifierGenerator.resolveChildUniqueIdentifier(it.first, it.second) } + .map { DefaultViewIdentifierResolver.resolveChildUniqueIdentifier(it.first, it.second) } .distinct() // Then @@ -54,11 +54,11 @@ internal class UniqueIdentifierGeneratorTest { val parentViews: List = forge.aList(numberOfCalls) { mock() } val uniqueNumbers = keyNames .mapIndexed { index, key -> parentViews[index] to key } - .map { UniqueIdentifierGenerator.resolveChildUniqueIdentifier(it.first, it.second) } + .map { DefaultViewIdentifierResolver.resolveChildUniqueIdentifier(it.first, it.second) } .distinct() parentViews.forEachIndexed { index, view -> - val keyName = UniqueIdentifierGenerator.DATADOG_UNIQUE_IDENTIFIER_KEY_PREFIX + + val keyName = DefaultViewIdentifierResolver.DATADOG_UNIQUE_IDENTIFIER_KEY_PREFIX + keyNames[index] whenever(view.getTag(keyName.hashCode())).thenReturn(uniqueNumbers[index]) } @@ -66,7 +66,7 @@ internal class UniqueIdentifierGeneratorTest { // When val secondTimeCallNumbers = keyNames .mapIndexed { index, key -> parentViews[index] to key } - .map { UniqueIdentifierGenerator.resolveChildUniqueIdentifier(it.first, it.second) } + .map { DefaultViewIdentifierResolver.resolveChildUniqueIdentifier(it.first, it.second) } .distinct() // Then @@ -85,11 +85,11 @@ internal class UniqueIdentifierGeneratorTest { // When val generatedUniqueNumbers = forge.aList(numberOfCalls) { mock() } .mapIndexed { index: Int, view: View -> - val keyName = UniqueIdentifierGenerator.DATADOG_UNIQUE_IDENTIFIER_KEY_PREFIX + + val keyName = DefaultViewIdentifierResolver.DATADOG_UNIQUE_IDENTIFIER_KEY_PREFIX + keyNames[index] whenever(view.getTag(keyName.hashCode())) .thenReturn(alreadyRegisteredValues[index]) - UniqueIdentifierGenerator.resolveChildUniqueIdentifier(view, keyNames[index]) + DefaultViewIdentifierResolver.resolveChildUniqueIdentifier(view, keyNames[index]) } assertThat(generatedUniqueNumbers).isEqualTo(alreadyRegisteredValues) } @@ -106,11 +106,11 @@ internal class UniqueIdentifierGeneratorTest { // When val generatedUniqueNumbers = forge.aList(numberOfCalls) { mock() } .mapIndexedNotNull { index: Int, view: View -> - val keyName = UniqueIdentifierGenerator.DATADOG_UNIQUE_IDENTIFIER_KEY_PREFIX + + val keyName = DefaultViewIdentifierResolver.DATADOG_UNIQUE_IDENTIFIER_KEY_PREFIX + keyNames[index] whenever(view.getTag(keyName.hashCode())) .thenReturn(alreadyRegisteredValues[index]) - UniqueIdentifierGenerator.resolveChildUniqueIdentifier(view, keyNames[index]) + DefaultViewIdentifierResolver.resolveChildUniqueIdentifier(view, keyNames[index]) } assertThat(generatedUniqueNumbers).isEmpty() @@ -124,7 +124,7 @@ internal class UniqueIdentifierGeneratorTest { // When val results = forge.aList(numberOfCalls) { mock() } .map { - UniqueIdentifierGenerator.resolveChildUniqueIdentifier(mock(), forge.aString()) + DefaultViewIdentifierResolver.resolveChildUniqueIdentifier(mock(), forge.aString()) } // Then diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapperTest.kt new file mode 100644 index 0000000000..1ad47e64a8 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapperTest.kt @@ -0,0 +1,159 @@ +package com.datadog.android.sessionreplay.utils + +//noinspection SuspiciousImport +import android.graphics.Paint +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.RippleDrawable +import com.datadog.android.sessionreplay.forge.ForgeConfigurator +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.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(value = ForgeConfigurator::class, seed = 0x3f3a03ceae05aL) +open class LegacyDrawableToColorMapperTest { + + lateinit var testedMapper: DrawableToColorMapper + + @BeforeEach + fun `set up`() { + testedMapper = createTestedMapper() + } + + open fun createTestedMapper(): DrawableToColorMapper { + return LegacyDrawableToColorMapper() + } + + @Test + fun `M map unknown Drawable to null W mapDrawableToColor()`() { + // Given + val colorDrawable = mock() + + // When + val result = testedMapper.mapDrawableToColor(colorDrawable) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M map ColorDrawable to color with alpha W mapDrawableToColor()`( + @IntForgery drawableColor: Int + ) { + // Given + val baseColor = drawableColor and 0xFFFFFF + val baseAlpha = (drawableColor.toLong() and 0xFF000000) shr 24 + val colorDrawable = mock().apply { + whenever(this.color) doReturn baseColor + whenever(this.alpha) doReturn baseAlpha.toInt() + } + + // When + val result = testedMapper.mapDrawableToColor(colorDrawable) + + // Then + assertThat(result).isEqualTo(drawableColor) + } + + @Test + fun `M map RippleDrawable to first opaque layer's color W mapDrawableToColor()`( + @IntForgery drawableColor: Int, + @IntForgery secondDrawableColor: Int + ) { + // Given + val unsupportedLayer = mock() + val baseColor = drawableColor and 0xFFFFFF + val baseAlpha = (drawableColor.toLong() and 0xFF000000) shr 24 + val opaqueLayer = mock().apply { + whenever(this.color) doReturn baseColor + whenever(this.alpha) doReturn baseAlpha.toInt() + } + val opaqueLayer2 = mock().apply { + whenever(this.color) doReturn (secondDrawableColor and 0xFFFFFF) + whenever(this.alpha) doReturn baseAlpha.toInt() + } + val rippleDrawable = mock().apply { + whenever(this.numberOfLayers) doReturn 4 + whenever(this.getDrawable(0)) doReturn unsupportedLayer + whenever(this.getDrawable(1)) doReturn opaqueLayer + whenever(this.getDrawable(2)) doReturn opaqueLayer2 + } + + // When + val result = testedMapper.mapDrawableToColor(rippleDrawable) + + // Then + assertThat(result).isEqualTo(drawableColor) + } + + @Test + fun `M map LayerDrawable to first opaque layer's color W mapDrawableToColor()`( + @IntForgery drawableColor: Int, + @IntForgery secondDrawableColor: Int + ) { + // Given + val unsupportedLayer = mock() + val baseColor = drawableColor and 0xFFFFFF + val baseAlpha = (drawableColor.toLong() and 0xFF000000) shr 24 + val opaqueLayer = mock().apply { + whenever(this.color) doReturn baseColor + whenever(this.alpha) doReturn baseAlpha.toInt() + } + val opaqueLayer2 = mock().apply { + whenever(this.color) doReturn (secondDrawableColor and 0xFFFFFF) + whenever(this.alpha) doReturn baseAlpha.toInt() + } + val layerDrawable = mock().apply { + whenever(this.numberOfLayers) doReturn 3 + whenever(this.getDrawable(0)) doReturn unsupportedLayer + whenever(this.getDrawable(1)) doReturn opaqueLayer + whenever(this.getDrawable(2)) doReturn opaqueLayer2 + } + + // When + val result = testedMapper.mapDrawableToColor(layerDrawable) + + // Then + assertThat(result).isEqualTo(drawableColor) + } + + @Test + fun `M map GradientDrawable to fill paint's color W mapDrawableToColor()`( + @IntForgery drawableColor: Int + ) { + // Given + val baseColor = drawableColor and 0xFFFFFF + val baseAlpha = (drawableColor.toLong() and 0xFF000000) shr 24 + val mockFillPaint = mock().apply { + whenever(this.color) doReturn baseColor + whenever(this.alpha) doReturn baseAlpha.toInt() + } + val gradientDrawable = GradientDrawable().apply { + LegacyDrawableToColorMapper.fillPaintField?.set(this, mockFillPaint) + } + + // When + val result = testedMapper.mapDrawableToColor(gradientDrawable) + + // Then + assertThat(result).isEqualTo(drawableColor) + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/StringUtilsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/StringUtilsTest.kt deleted file mode 100644 index 1ffb801c5c..0000000000 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/StringUtilsTest.kt +++ /dev/null @@ -1,34 +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.sessionreplay.utils - -import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import fr.xgouchet.elmyr.annotation.StringForgery -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 -import org.junit.jupiter.api.extension.Extensions - -@Extensions(ExtendWith(ForgeExtension::class)) -@ForgeConfiguration(ForgeConfigurator::class) -internal class StringUtilsTest { - - @Test - fun `M return hex formatted color W formatColorAndAlphaAsHexa`( - @StringForgery(regex = "#[0-9a-f]{8}") fakeExpectedFormattedColor: String - ) { - // Given - val color = fakeExpectedFormattedColor.substring(1 until 7).toInt(16) - val colorAlpha = fakeExpectedFormattedColor.substring(7..8).toInt(16) - - // Then - assertThat(StringUtils.formatColorAndAlphaAsHexa(color, colorAlpha)) - .isEqualTo(fakeExpectedFormattedColor) - } -} diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/OtelTracerProvider.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/OtelTracerProvider.kt index fce85e11e8..34079345fd 100644 --- a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/OtelTracerProvider.kt +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/OtelTracerProvider.kt @@ -241,11 +241,11 @@ class OtelTracerProvider( internal const val TRACING_NOT_ENABLED_ERROR_MESSAGE = "You're trying to create an OtelTracerProvider instance, " + - "but either the SDK was not initialized or the Tracing feature was " + - "disabled in your Configuration. No tracing data will be sent." + "but either the SDK was not initialized or the Tracing feature was " + + "disabled in your Configuration. No tracing data will be sent." internal const val DEFAULT_SERVICE_NAME_IS_MISSING_ERROR_MESSAGE = "Default service name is missing during" + - " OtelTracerProvider creation, did you initialize SDK?" + " OtelTracerProvider creation, did you initialize SDK?" // the minimum closed spans required for triggering a flush and deliver // everything to the writer diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/data/OtelTraceWriter.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/data/OtelTraceWriter.kt index 72d680c863..6d01e6a106 100644 --- a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/data/OtelTraceWriter.kt +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/data/OtelTraceWriter.kt @@ -38,12 +38,12 @@ internal class OtelTraceWriter( override fun write(trace: MutableList?) { if (trace == null) return sdkCore.getFeature(Feature.TRACING_FEATURE_NAME) - ?.withWriteContext { datadogContext, eventBatchWriter -> - trace.forEach { span -> - @Suppress("ThreadSafety") // called in the worker context - writeSpan(datadogContext, eventBatchWriter, span) - } + ?.withWriteContext { datadogContext, eventBatchWriter -> + trace.forEach { span -> + @Suppress("ThreadSafety") // called in the worker context + writeSpan(datadogContext, eventBatchWriter, span) } + } } override fun flush(): Boolean { diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/assertj/TracerConfigAssert.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/assertj/TracerConfigAssert.kt index a202510a00..b1caeffecf 100644 --- a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/assertj/TracerConfigAssert.kt +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/assertj/TracerConfigAssert.kt @@ -11,7 +11,7 @@ import org.assertj.core.api.AbstractObjectAssert import org.assertj.core.api.Assertions.assertThat internal class TracerConfigAssert(actual: Config) : AbstractObjectAssert -(actual, TracerConfigAssert::class.java) { + (actual, TracerConfigAssert::class.java) { fun hasServiceName(expected: String): TracerConfigAssert { assertThat(actual.serviceName) diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/domain/event/OtelDdSpanToSpanEventMapperTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/domain/event/OtelDdSpanToSpanEventMapperTest.kt index 453898bb0f..d7323aded0 100644 --- a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/domain/event/OtelDdSpanToSpanEventMapperTest.kt +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/domain/event/OtelDdSpanToSpanEventMapperTest.kt @@ -30,7 +30,8 @@ import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @Extensions( - ExtendWith(MockitoExtension::class), ExtendWith(ForgeExtension::class) + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) ) @MockitoSettings(strictness = Strictness.LENIENT) @ForgeConfiguration(Configurator::class) @@ -206,7 +207,8 @@ internal class OtelDdSpanToSpanEventMapperTest { @Test fun `M not mark the SpanEvent as top span W map() { parentId is different than 0 }`( - forge: Forge, @Forgery fakeSpan: DDSpan + forge: Forge, + @Forgery fakeSpan: DDSpan ) { // Given whenever(fakeSpan.parentId).thenReturn(forge.aLong(min = 1)) diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/CoreSpanBuilderTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/CoreSpanBuilderTest.kt index 14dbbfdccc..0c9c73f6b2 100644 --- a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/CoreSpanBuilderTest.kt +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/CoreSpanBuilderTest.kt @@ -354,15 +354,15 @@ internal class CoreSpanBuilderTest : DDCoreSpecification() { assertThat(span.context().baggageItems).isEqualTo(emptyMap()) assertThat(span.context().tags).containsExactlyInAnyOrderEntriesOf( tagContext.tags + - mapOf( - DDTags.RUNTIME_ID_TAG to Config.get().getRuntimeId(), - DDTags.LANGUAGE_TAG_KEY to DDTags.LANGUAGE_TAG_VALUE, - DDTags.THREAD_NAME to thread.name, - DDTags.THREAD_ID to thread.id, - DDTags.PID_TAG to Config.get().processId, - DDTags.SCHEMA_VERSION_TAG_KEY to SpanNaming.instance().version(), - DDTags.PROFILING_ENABLED to if (Config.get().isProfilingEnabled()) 1 else 0 - ) + mapOf( + DDTags.RUNTIME_ID_TAG to Config.get().getRuntimeId(), + DDTags.LANGUAGE_TAG_KEY to DDTags.LANGUAGE_TAG_VALUE, + DDTags.THREAD_NAME to thread.name, + DDTags.THREAD_ID to thread.id, + DDTags.PID_TAG to Config.get().processId, + DDTags.SCHEMA_VERSION_TAG_KEY to SpanNaming.instance().version(), + DDTags.PROFILING_ENABLED to if (Config.get().isProfilingEnabled()) 1 else 0 + ) ) } @@ -378,15 +378,15 @@ internal class CoreSpanBuilderTest : DDCoreSpecification() { // Then assertThat(span.tags).containsExactlyInAnyOrderEntriesOf( tags + - mapOf( - DDTags.THREAD_NAME to Thread.currentThread().name, - DDTags.THREAD_ID to Thread.currentThread().id, - DDTags.RUNTIME_ID_TAG to Config.get().getRuntimeId(), - DDTags.LANGUAGE_TAG_KEY to DDTags.LANGUAGE_TAG_VALUE, - DDTags.PID_TAG to Config.get().getProcessId(), - DDTags.SCHEMA_VERSION_TAG_KEY to SpanNaming.instance().version(), - DDTags.PROFILING_ENABLED to if (Config.get().isProfilingEnabled()) 1 else 0 - ) + mapOf( + DDTags.THREAD_NAME to Thread.currentThread().name, + DDTags.THREAD_ID to Thread.currentThread().id, + DDTags.RUNTIME_ID_TAG to Config.get().getRuntimeId(), + DDTags.LANGUAGE_TAG_KEY to DDTags.LANGUAGE_TAG_VALUE, + DDTags.PID_TAG to Config.get().getProcessId(), + DDTags.SCHEMA_VERSION_TAG_KEY to SpanNaming.instance().version(), + DDTags.PROFILING_ENABLED to if (Config.get().isProfilingEnabled()) 1 else 0 + ) ) // Tear down diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/DDSpanContextTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/DDSpanContextTest.kt index 984e5b22b8..d1a867ab46 100644 --- a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/DDSpanContextTest.kt +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/DDSpanContextTest.kt @@ -89,8 +89,8 @@ internal class DDSpanContextTest : DDCoreSpecification() { val expectedTags = mapOf(DDTags.THREAD_NAME to thread.name, DDTags.THREAD_ID to thread.id) assertThat(context.tags).containsAllEntriesOf(expectedTags) assertThat( - context::class.java.getMethod(method) - .invoke(context) + context::class.java.getMethod(method) + .invoke(context) ) .isEqualTo(value) } diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/DDSpanTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/DDSpanTest.kt index 464503c666..464db52566 100644 --- a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/DDSpanTest.kt +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/DDSpanTest.kt @@ -151,7 +151,7 @@ internal class DDSpanTest : DDCoreSpecification() { // Then val timeDifference = TimeUnit.NANOSECONDS.toSeconds(span.startTime) - - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) assertThat(timeDifference).isLessThan(5) assertThat(span.durationNano).isGreaterThan(betweenDur) assertThat(span.durationNano).isLessThan(total) @@ -184,7 +184,7 @@ internal class DDSpanTest : DDCoreSpecification() { assertThat(writer).isEmpty() val actualDurationNano = span.durationNano and Long.MAX_VALUE val timeDifference = TimeUnit.NANOSECONDS.toSeconds(span.startTime) - - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) assertThat(timeDifference).isLessThan(5) assertThat(actualDurationNano).isGreaterThan(betweenDur) assertThat(actualDurationNano).isLessThan(total) @@ -230,7 +230,7 @@ internal class DDSpanTest : DDCoreSpecification() { // Then val total = Math.max(1, System.currentTimeMillis() - start) val timeDifference = TimeUnit.NANOSECONDS.toSeconds(span.startTime) - - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) assertThat(timeDifference).isLessThan(5) assertThat(span.durationNano).isGreaterThanOrEqualTo(TimeUnit.MILLISECONDS.toNanos(betweenDur)) assertThat(span.durationNano).isLessThanOrEqualTo(TimeUnit.MILLISECONDS.toNanos(total)) @@ -251,7 +251,7 @@ internal class DDSpanTest : DDCoreSpecification() { span.finish(TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis() + 1)) val total = System.currentTimeMillis() - start + 1 val timeDifference = TimeUnit.NANOSECONDS.toSeconds(span.startTime) - - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) // Then assertThat(timeDifference).isLessThan(5) diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/PendingTraceTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/PendingTraceTest.kt index 9c8e1473ec..e4b3ddba48 100644 --- a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/PendingTraceTest.kt +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/PendingTraceTest.kt @@ -193,7 +193,8 @@ internal class PendingTraceTest : PendingTraceTestBase() { private fun createSimpleSpanWithID(trace: PendingTrace, id: Long): DDSpan { return DDSpan( - "test", 0L, + "test", + 0L, DDSpanContext( DDTraceId.from(1), id, diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/PendingTraceTestBase.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/PendingTraceTestBase.kt index ad02b38085..637aaedfad 100644 --- a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/PendingTraceTestBase.kt +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/trace/core/PendingTraceTestBase.kt @@ -136,7 +136,7 @@ internal abstract class PendingTraceTestBase : DDCoreSpecification() { assertThat( Math.abs( TimeUnit.NANOSECONDS.toSeconds(trace.currentTimeNano) - - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) ) ).isLessThan(5) } diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/consent_granted_sr_test_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/consent_granted_sr_test_payload.json index 5f9bcf76b1..b1f15ba5ea 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/consent_granted_sr_test_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/consent_granted_sr_test_payload.json @@ -48,9 +48,11 @@ { "width": 88, "height": 48, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false + "shapeStyle": { + "backgroundColor": "#5a595bff", + "opacity": 1.0 + }, + "type": "shape" }, { "width": 88, diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_image_buttons_allow_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_image_buttons_allow_payload.json index 69db4997be..e2b8024ddd 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_image_buttons_allow_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_image_buttons_allow_payload.json @@ -25,9 +25,11 @@ { "width": 80, "height": 80, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false + "shapeStyle": { + "backgroundColor": "#5a595bff", + "opacity": 1.0 + }, + "type": "shape" }, { "width": 80, @@ -39,15 +41,16 @@ "right": 0 }, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { "width": 80, "height": 80, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false + "shapeStyle": { + "backgroundColor": "#5a595bff", + "opacity": 1.0 + }, + "type": "shape" }, { "width": 80, @@ -59,15 +62,16 @@ "right": 0 }, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { "width": 80, "height": 80, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false + "shapeStyle": { + "backgroundColor": "#5a595bff", + "opacity": 1.0 + }, + "type": "shape" }, { "width": 80, @@ -79,7 +83,6 @@ "right": 0 }, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_image_buttons_mask_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_image_buttons_mask_payload.json index 69db4997be..e2b8024ddd 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_image_buttons_mask_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_image_buttons_mask_payload.json @@ -25,9 +25,11 @@ { "width": 80, "height": 80, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false + "shapeStyle": { + "backgroundColor": "#5a595bff", + "opacity": 1.0 + }, + "type": "shape" }, { "width": 80, @@ -39,15 +41,16 @@ "right": 0 }, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { "width": 80, "height": 80, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false + "shapeStyle": { + "backgroundColor": "#5a595bff", + "opacity": 1.0 + }, + "type": "shape" }, { "width": 80, @@ -59,15 +62,16 @@ "right": 0 }, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { "width": 80, "height": 80, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false + "shapeStyle": { + "backgroundColor": "#5a595bff", + "opacity": 1.0 + }, + "type": "shape" }, { "width": 80, @@ -79,7 +83,6 @@ "right": 0 }, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_image_buttons_mask_user_input_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_image_buttons_mask_user_input_payload.json index 69db4997be..1a46f725db 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_image_buttons_mask_user_input_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_image_buttons_mask_user_input_payload.json @@ -1,93 +1,103 @@ -[{ - "type": 4, - "data": { - "width": 411, - "height": 914 - } -}, { - "type": 6, - "data": { - "has_focus": true - } -}, { - "type": 10, - "data": { - "wireframes": [ - { - "width": 411, - "height": 914, - "shapeStyle": { - "backgroundColor": "#303030ff", - "opacity": 1.0 +[ + { + "type": 4, + "data": { + "width": 411, + "height": 914 + } + }, + { + "type": 6, + "data": { + "has_focus": true + } + }, + { + "type": 10, + "data": { + "wireframes": [ + { + "width": 411, + "height": 914, + "shapeStyle": { + "backgroundColor": "#303030ff", + "opacity": 1.0 + }, + "type": "shape" + }, + { + "width": 80, + "height": 80, + "shapeStyle": { + "backgroundColor": "#ffffffff", + "opacity": 1.0 + }, + "type": "shape" + }, + { + "width": 80, + "height": 84, + "clip": { + "top": 2, + "bottom": 2, + "left": 0, + "right": 0 + }, + "type": "image", + "mimeType": "image/webp", + "isEmpty": false + }, + { + "width": 80, + "height": 80, + "shapeStyle": { + "backgroundColor": "#ffffffff", + "opacity": 1.0 + }, + "type": "shape" }, - "type": "shape" - }, - { - "width": 80, - "height": 80, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false - }, - { - "width": 80, - "height": 84, - "clip": { - "top": 2, - "bottom": 2, - "left": 0, - "right": 0 + { + "width": 80, + "height": 84, + "clip": { + "top": 2, + "bottom": 2, + "left": 0, + "right": 0 + }, + "type": "image", + "mimeType": "image/webp", + "isEmpty": false }, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false - }, - { - "width": 80, - "height": 80, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false - }, - { - "width": 80, - "height": 84, - "clip": { - "top": 2, - "bottom": 2, - "left": 0, - "right": 0 + { + "width": 80, + "height": 80, + "shapeStyle": { + "backgroundColor": "#ffffffff", + "opacity": 1.0 + }, + "type": "shape" }, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false - }, - { - "width": 80, - "height": 80, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false - }, - { - "width": 80, - "height": 84, - "clip": { - "top": 2, - "bottom": 2, - "left": 0, - "right": 0 + { + "width": 80, + "height": 84, + "clip": { + "top": 2, + "bottom": 2, + "left": 0, + "right": 0 + }, + "type": "image", + "mimeType": "image/webp", + "isEmpty": false }, - "type": "image", - "mimeType": "image/webp", - "isEmpty": false - }, - { - "width": 411, - "height": 56, - "type": "placeholder", - "label": "Toolbar" - } - ] + { + "width": 411, + "height": 56, + "type": "placeholder", + "label": "Toolbar" + } + ] + } } -}] \ No newline at end of file +] \ No newline at end of file diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_images_allow_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_images_allow_payload.json index 46a42a13b6..8703072656 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_images_allow_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_images_allow_payload.json @@ -32,7 +32,6 @@ "right": 0 }, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -45,7 +44,6 @@ "right": 0 }, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_images_mask_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_images_mask_payload.json index 46a42a13b6..8703072656 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_images_mask_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_images_mask_payload.json @@ -32,7 +32,6 @@ "right": 0 }, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -45,7 +44,6 @@ "right": 0 }, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_images_mask_user_input_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_images_mask_user_input_payload.json index 46a42a13b6..8703072656 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_images_mask_user_input_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_images_mask_user_input_payload.json @@ -32,7 +32,6 @@ "right": 0 }, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -45,7 +44,6 @@ "right": 0 }, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_sensitive_fields_allow_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_sensitive_fields_allow_payload.json index 3fb3561ceb..9d7e8f4be8 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_sensitive_fields_allow_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_sensitive_fields_allow_payload.json @@ -26,7 +26,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -56,7 +55,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -86,7 +84,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -116,7 +113,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -146,7 +142,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -176,7 +171,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -206,7 +200,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_sensitive_fields_mask_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_sensitive_fields_mask_payload.json index 3fb3561ceb..9d7e8f4be8 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_sensitive_fields_mask_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_sensitive_fields_mask_payload.json @@ -26,7 +26,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -56,7 +55,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -86,7 +84,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -116,7 +113,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -146,7 +142,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -176,7 +171,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -206,7 +200,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_sensitive_fields_mask_user_input_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_sensitive_fields_mask_user_input_payload.json index 3fb3561ceb..9d7e8f4be8 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_sensitive_fields_mask_user_input_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_sensitive_fields_mask_user_input_payload.json @@ -26,7 +26,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -56,7 +55,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -86,7 +84,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -116,7 +113,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -146,7 +142,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -176,7 +171,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -206,7 +200,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_allow_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_allow_payload.json index 60d42b0b0e..209b4654df 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_allow_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_allow_payload.json @@ -95,7 +95,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -125,7 +124,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -155,7 +153,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -185,7 +182,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -215,7 +211,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_mask_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_mask_payload.json index 7072b9fea7..61ef0846e6 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_mask_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_mask_payload.json @@ -95,7 +95,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -125,7 +124,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -155,7 +153,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -185,7 +182,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -215,7 +211,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_mask_user_input_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_mask_user_input_payload.json index 44fdf5109e..489126f73f 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_mask_user_input_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_mask_user_input_payload.json @@ -95,7 +95,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -125,7 +124,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -155,7 +153,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -185,7 +182,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -215,7 +211,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { diff --git a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_with_input_mask_user_input_payload.json b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_with_input_mask_user_input_payload.json index ae1a2969e4..d00e932eb4 100644 --- a/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_with_input_mask_user_input_payload.json +++ b/instrumented/integration/src/androidTest/assets/session_replay_payloads/sr_text_fields_with_input_mask_user_input_payload.json @@ -95,7 +95,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -125,7 +124,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -155,7 +153,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -185,7 +182,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { @@ -215,7 +211,6 @@ "width": 395, "height": 44, "type": "image", - "mimeType": "image/webp", "isEmpty": false }, { diff --git a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/ActivityTrackingTest.kt b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/ActivityTrackingTest.kt index 18f6ebddcd..fb57413f8b 100644 --- a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/ActivityTrackingTest.kt +++ b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/ActivityTrackingTest.kt @@ -12,9 +12,9 @@ import com.datadog.android.sdk.rules.RumMockServerActivityTestRule internal abstract class ActivityTrackingTest : RumTest< - ActivityTrackingPlaygroundActivity, - RumMockServerActivityTestRule - >() { + ActivityTrackingPlaygroundActivity, + RumMockServerActivityTestRule + >() { // region RumTest diff --git a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/FragmentTrackingTest.kt b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/FragmentTrackingTest.kt index 406e459483..e588b04e28 100644 --- a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/FragmentTrackingTest.kt +++ b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/FragmentTrackingTest.kt @@ -17,9 +17,9 @@ import com.datadog.android.sdk.utils.asMap internal abstract class FragmentTrackingTest : RumTest< - FragmentTrackingPlaygroundActivity, - RumMockServerActivityTestRule - >() { + FragmentTrackingPlaygroundActivity, + RumMockServerActivityTestRule + >() { // region RumTest diff --git a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/GesturesTrackingTest.kt b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/GesturesTrackingTest.kt index 3f855b9b19..d3e162b34d 100644 --- a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/GesturesTrackingTest.kt +++ b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/GesturesTrackingTest.kt @@ -20,9 +20,9 @@ import com.datadog.android.sdk.rules.GesturesTrackingActivityTestRule internal abstract class GesturesTrackingTest : RumTest< - GesturesTrackingPlaygroundActivity, - GesturesTrackingActivityTestRule - >() { + GesturesTrackingPlaygroundActivity, + GesturesTrackingActivityTestRule + >() { // region RumTest @@ -92,9 +92,9 @@ internal abstract class GesturesTrackingTest : extraAttributes = mapOf( RumAttributes.ACTION_TARGET_PARENT_INDEX to 2, RumAttributes.ACTION_TARGET_PARENT_CLASSNAME to - activity.recyclerView.javaClass.canonicalName, + activity.recyclerView.javaClass.canonicalName, RumAttributes.ACTION_TARGET_PARENT_RESOURCE_ID to - "recyclerView" + "recyclerView" ) ), ExpectedGestureEvent( diff --git a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/KioskTrackingTest.kt b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/KioskTrackingTest.kt index b9831a4c49..afbdb8522b 100644 --- a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/KioskTrackingTest.kt +++ b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/rum/KioskTrackingTest.kt @@ -25,9 +25,9 @@ import org.junit.runner.RunWith @LargeTest internal class KioskTrackingTest : RumTest< - KioskSplashPlaygroundActivity, - RumMockServerActivityTestRule - >() { + KioskSplashPlaygroundActivity, + RumMockServerActivityTestRule + >() { @get:Rule val mockServerRule = KioskTrackingActivityTestRule( diff --git a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/sessionreplay/ConsentGrantedSrTest.kt b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/sessionreplay/ConsentGrantedSrTest.kt index cfa45b7654..f838a44f71 100644 --- a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/sessionreplay/ConsentGrantedSrTest.kt +++ b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/sessionreplay/ConsentGrantedSrTest.kt @@ -8,8 +8,8 @@ package com.datadog.android.sdk.integration.sessionreplay import com.datadog.android.privacy.TrackingConsent import com.datadog.android.sdk.rules.SessionReplayTestRule +import org.junit.Ignore import org.junit.Rule -import org.junit.Test internal class ConsentGrantedSrTest : BaseSessionReplayTest() { @@ -20,7 +20,7 @@ internal class ConsentGrantedSrTest : BaseSessionReplayTest() { @@ -26,11 +26,12 @@ internal class SrImageButtonsMaskTest : intentExtras = mapOf(SR_PRIVACY_LEVEL to SessionReplayPrivacy.MASK) ) - @Test + @Ignore("Flakiness in CI, unsolved yet") fun assessRecordedScreenPayload() { runInstrumentationScenario() assessSrPayload(EXPECTED_PAYLOAD_FILE_NAME, rule) } + companion object { const val EXPECTED_PAYLOAD_FILE_NAME = "sr_image_buttons_mask_payload.json" } diff --git a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/sessionreplay/imagebuttons/SrImageButtonsMaskUserInputTest.kt b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/sessionreplay/imagebuttons/SrImageButtonsMaskUserInputTest.kt index 4e646177fe..c63b2aab0e 100644 --- a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/sessionreplay/imagebuttons/SrImageButtonsMaskUserInputTest.kt +++ b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/sessionreplay/imagebuttons/SrImageButtonsMaskUserInputTest.kt @@ -12,8 +12,8 @@ import com.datadog.android.sdk.integration.sessionreplay.SessionReplayImageButto import com.datadog.android.sdk.rules.SessionReplayTestRule import com.datadog.android.sdk.utils.SR_PRIVACY_LEVEL import com.datadog.android.sessionreplay.SessionReplayPrivacy +import org.junit.Ignore import org.junit.Rule -import org.junit.Test internal class SrImageButtonsMaskUserInputTest : BaseSessionReplayTest() { @@ -26,11 +26,12 @@ internal class SrImageButtonsMaskUserInputTest : intentExtras = mapOf(SR_PRIVACY_LEVEL to SessionReplayPrivacy.MASK_USER_INPUT) ) - @Test + @Ignore("Flakiness in CI, unsolved yet") fun assessRecordedScreenPayload() { runInstrumentationScenario() assessSrPayload(EXPECTED_PAYLOAD_FILE_NAME, rule) } + companion object { const val EXPECTED_PAYLOAD_FILE_NAME = "sr_image_buttons_mask_user_input_payload.json" } diff --git a/instrumented/nightly-tests/src/androidTest/kotlin/com/datadog/android/nightly/main/NotTestableApis.kt b/instrumented/nightly-tests/src/androidTest/kotlin/com/datadog/android/nightly/main/NotTestableApis.kt index 50307d3979..6be4b9b141 100644 --- a/instrumented/nightly-tests/src/androidTest/kotlin/com/datadog/android/nightly/main/NotTestableApis.kt +++ b/instrumented/nightly-tests/src/androidTest/kotlin/com/datadog/android/nightly/main/NotTestableApis.kt @@ -52,6 +52,7 @@ package com.datadog.android.nightly.main * apiMethodSignature: com.datadog.android.sessionreplay.SessionReplayConfiguration$Builder#fun build(): SessionReplayConfiguration * apiMethodSignature: com.datadog.android.sessionreplay.SessionReplayConfiguration$Builder#fun setPrivacy(SessionReplayPrivacy): Builder * apiMethodSignature: com.datadog.android.sessionreplay.SessionReplayConfiguration$Builder#fun useCustomEndpoint(String): Builder + * apiMethodSignature: fun Array.loggableStackTrace(): String * apiMethodSignature: fun Collection.join(ByteArray, ByteArray = ByteArray(0), ByteArray = ByteArray(0), com.datadog.android.api.InternalLogger): ByteArray * apiMethodSignature: fun java.math.BigInteger.toHexString(): String * apiMethodSignature: fun java.util.concurrent.Executor.executeSafe(String, com.datadog.android.api.InternalLogger, Runnable) @@ -59,6 +60,7 @@ package com.datadog.android.nightly.main * apiMethodSignature: fun java.util.concurrent.ScheduledExecutorService.scheduleSafe(String, Long, java.util.concurrent.TimeUnit, com.datadog.android.api.InternalLogger, Runnable): java.util.concurrent.ScheduledFuture<*>? * apiMethodSignature: fun Int.toHexString(): String * apiMethodSignature: fun Long.toHexString(): String + * apiMethodSignature: fun Thread.State.asString(): String * apiMethodSignature: fun Throwable.loggableStackTrace(): String * apiMethodSignature: fun T.useMonitored(com.datadog.android.api.SdkCore = Datadog.getInstance(), (T) -> R): R * apiMethodSignature: fun allowThreadDiskReads(() -> T): T diff --git a/reliability/single-fit/logs/src/test/kotlin/com/datadog/android/logs/integration/LoggerTest.kt b/reliability/single-fit/logs/src/test/kotlin/com/datadog/android/logs/integration/LoggerTest.kt index b8d6583237..6d015b151a 100644 --- a/reliability/single-fit/logs/src/test/kotlin/com/datadog/android/logs/integration/LoggerTest.kt +++ b/reliability/single-fit/logs/src/test/kotlin/com/datadog/android/logs/integration/LoggerTest.kt @@ -13,6 +13,7 @@ import com.datadog.android.api.feature.StorageBackedFeature import com.datadog.android.api.storage.RawBatchEvent import com.datadog.android.core.stub.StubSDKCore import com.datadog.android.event.EventMapper +import com.datadog.android.log.LogAttributes import com.datadog.android.log.Logger import com.datadog.android.log.Logs import com.datadog.android.log.LogsConfiguration @@ -703,6 +704,38 @@ class LoggerTest { assertThat(event0.getString("error.stack")).isEqualTo(fakeErrorStack) } + @RepeatedTest(16) + fun `M send log with custom error fingerprint W Logger#log()`( + @StringForgery fakeErrorKind: String, + @StringForgery fakeErrorMessage: String, + @StringForgery fakeErrorStack: String, + @StringForgery fakeMessage: String, + @StringForgery fakeFingerprint: String, + @IntForgery(Log.VERBOSE, 10) fakeLevel: Int + ) { + // Given + val testedLogger = Logger.Builder(stubSdkCore).build() + + // When + val attributes = mapOf(LogAttributes.ERROR_FINGERPRINT to fakeFingerprint) + testedLogger.log(fakeLevel, fakeMessage, fakeErrorKind, fakeErrorMessage, fakeErrorStack, attributes) + + // Then + val eventsWritten = stubSdkCore.eventsWritten(Feature.LOGS_FEATURE_NAME) + assertThat(eventsWritten).hasSize(1) + val event0 = JsonParser.parseString(eventsWritten[0].eventData) as JsonObject + assertThat(event0.getString("ddtags")).contains("env:" + stubSdkCore.getDatadogContext().env) + assertThat(event0.getString("ddtags")).contains("version:" + stubSdkCore.getDatadogContext().version) + assertThat(event0.getString("ddtags")).contains("variant:" + stubSdkCore.getDatadogContext().variant) + assertThat(event0.getString("service")).isEqualTo(stubSdkCore.getDatadogContext().service) + assertThat(event0.getString("message")).isEqualTo(fakeMessage) + assertThat(event0.getString("status")).isEqualTo(LEVEL_NAMES[fakeLevel]) + assertThat(event0.getString("error.kind")).isEqualTo(fakeErrorKind) + assertThat(event0.getString("error.message")).isEqualTo(fakeErrorMessage) + assertThat(event0.getString("error.stack")).isEqualTo(fakeErrorStack) + assertThat(event0.getString("error.fingerprint")).isEqualTo(fakeFingerprint) + } + @RepeatedTest(16) fun `M send log with custom attribute W Logger#addAttribute() + Logger#log()`( @StringForgery fakeAttributeKey: String, diff --git a/sample/kotlin/src/main/kotlin/com/datadog/android/sample/SampleApplication.kt b/sample/kotlin/src/main/kotlin/com/datadog/android/sample/SampleApplication.kt index 4bac205868..de0a5e8602 100644 --- a/sample/kotlin/src/main/kotlin/com/datadog/android/sample/SampleApplication.kt +++ b/sample/kotlin/src/main/kotlin/com/datadog/android/sample/SampleApplication.kt @@ -217,6 +217,7 @@ class SampleApplication : Application() { .setTelemetrySampleRate(100f) .trackUserInteractions() .trackLongTasks(250L) + .trackNonFatalAnrs(true) .setViewEventMapper(object : ViewEventMapper { override fun map(event: ViewEvent): ViewEvent { event.context?.additionalProperties?.put(ATTR_IS_MAPPED, true)