Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ buildscript {
classpath(libs.sqlDelightGradlePlugin)
classpath(libs.binaryCompatibilityGradlePlugin)
classpath(libs.kotlinxSerializationPlugin)
classpath(libs.shadowPlugin)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object AndroidConfig {
}
// Local development or other branches → Snapshot
else -> {
Version(0, 3, 0, Version.Type.Snapshot)
Version(0, 3, 1, Version.Type.Snapshot)
}
}
}
Expand Down Expand Up @@ -77,6 +77,7 @@ fun Project.androidLibraryConfig() {

defaultConfig {
minSdk = AndroidConfig.MIN_SDK
consumerProguardFiles("${rootDir.absolutePath}/consumer-rules.pro")
}

compileOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,17 +122,28 @@ fun Project.detektCustomConfig() {
}
}

val externalDependencies = File("${projectDir.absolutePath}/detekt_classpath").readText()
val externalDependenciesFile = File("${projectDir.absolutePath}/detekt_classpath")
val externalDependencies = if (externalDependenciesFile.exists()) {
externalDependenciesFile.readText()
} else {
logger.warn("Detekt classpath file missing: ${externalDependenciesFile.path}")
""
}
val moduleDependenciesClasses = moduleDependencies.map {
"${rootDir.absolutePath}${it.replace(':', '/')}/build/extracted/classes.jar"
}.joinToString(":")

val dependencies = if (moduleDependenciesClasses.isBlank()) {
externalDependencies
} else {
"$externalDependencies:$moduleDependenciesClasses"
if (externalDependencies.isBlank()) {
moduleDependenciesClasses
} else {
"$externalDependencies:$moduleDependenciesClasses"
}
}


args("-cp", dependencies)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ fun Project.publishingConfig(
}

signingExtension.apply {
// Signing is required unless explicitly skipped
isRequired = !hasProperty("dd-skip-signing")
// Signing is required unless explicitly skipped or publishing to maven local
val isLocalPublish = gradle.startParameter.taskNames.any {
it.contains("publishToMavenLocal", ignoreCase = true)
}
isRequired = !hasProperty("dd-skip-signing") && !isLocalPublish

val privateKey = System.getenv("GPG_PRIVATE_KEY")
val password = System.getenv("GPG_PASSWORD")
Expand Down
3 changes: 3 additions & 0 deletions consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# This is needed for the Datadog Error Tracking feature to work reliably,
# this file is used by Logs and RUM modules
-keepattributes SourceFile,LineNumberTable

# Shaded dependencies
-keep class cloud.flashcat.shaded.** { *; }
2 changes: 2 additions & 0 deletions dd-sdk-android-core/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ fun <T> java.util.concurrent.Future<T>?.getSafe(String, com.datadog.android.api.
object com.datadog.android.core.internal.utils.JsonSerializer
fun toJsonElement(Any?): com.google.gson.JsonElement
fun Map<String, Any?>.safeMapValuesToJson(com.datadog.android.api.InternalLogger): Map<String, com.google.gson.JsonElement>
fun isInitialized(android.content.Context): Boolean
fun getWorkManagerOrNull(android.content.Context): androidx.work.WorkManager?
enum com.datadog.android.core.metrics.MethodCallSamplingRate
constructor(Float)
- ALL
Expand Down
5 changes: 5 additions & 0 deletions dd-sdk-android-core/api/dd-sdk-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,11 @@ public final class com/datadog/android/core/internal/utils/JsonSerializer {
public final fun toJsonElement (Ljava/lang/Object;)Lcom/google/gson/JsonElement;
}

public final class com/datadog/android/core/internal/utils/WorkManagerUtilsKt {
public static final fun getWorkManagerOrNull (Landroid/content/Context;)Landroidx/work/WorkManager;
public static final fun isInitialized (Landroid/content/Context;)Z
}

public final class com/datadog/android/core/metrics/MethodCallSamplingRate : java/lang/Enum {
public static final field ALL Lcom/datadog/android/core/metrics/MethodCallSamplingRate;
public static final field HIGH Lcom/datadog/android/core/metrics/MethodCallSamplingRate;
Expand Down
10 changes: 7 additions & 3 deletions dd-sdk-android-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,15 @@ dependencies {
implementation(libs.kotlin)

// Network
implementation(libs.okHttp)
implementation(libs.gson)
compileOnly(libs.okHttp)
compileOnly(libs.gson)
implementation(project(":dd-sdk-android-dependencies"))
implementation(libs.kronosNTP)

// Android Instrumentation
implementation(libs.androidXAnnotation)
implementation(libs.androidXCollection)
implementation(libs.androidXWorkManager)
compileOnly(libs.androidXWorkManager)

implementation(project(":dd-sdk-android-internal"))

Expand All @@ -130,6 +131,9 @@ dependencies {
}
}
testImplementation(testFixtures(project(":dd-sdk-android-internal")))
testImplementation(libs.gson)
testImplementation(libs.okHttp)
testImplementation(libs.androidXWorkManager)
testImplementation(libs.bundles.jUnit5)
testImplementation(libs.bundles.testTools)
unmock(libs.robolectric)
Expand Down
56 changes: 56 additions & 0 deletions dd-sdk-android-core/src/main/kotlin/com/datadog/android/Datadog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package com.datadog.android

import android.content.Context
import android.util.Log
import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import com.datadog.android.Datadog.clearAccountInfo
Expand Down Expand Up @@ -63,6 +64,7 @@ object Datadog {
configuration: Configuration,
trackingConsent: TrackingConsent
): SdkCore? {
checkRuntimeDependencies()
synchronized(registry) {
val existing = registry.getInstance(instanceName)
if (existing != null) {
Expand Down Expand Up @@ -432,6 +434,53 @@ object Datadog {

// endregion

// region Internal

private fun checkRuntimeDependencies() {
val missingDependencies = mutableListOf<String>()
if (!isClassAvailable("com.google.gson.Gson")) {
missingDependencies.add("Gson (com.google.code.gson:gson)")
}
if (!isClassAvailable("okhttp3.OkHttpClient")) {
missingDependencies.add("OkHttp (com.squareup.okhttp3:okhttp)")
}

if (missingDependencies.isNotEmpty()) {
val message = MISSING_DEPENDENCIES_ERROR.format(
Locale.US,
missingDependencies.joinToString(", ")
)
android.util.Log.e("Datadog", message)
unboundInternalLogger.log(
InternalLogger.Level.ERROR,
InternalLogger.Target.USER,
{ message }
)
throw IllegalStateException(message)
}

if (!isClassAvailable("androidx.work.WorkManager")) {
unboundInternalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.USER,
{ WARNING_WORKMANAGER_MISSING }
)
}
}

private fun isClassAvailable(className: String): Boolean {
return try {
Class.forName(className)
true
} catch (e: ClassNotFoundException) {
false
} catch (e: LinkageError) {
false
}
}

// endregion

// region Constants

internal const val MESSAGE_ALREADY_INITIALIZED =
Expand All @@ -445,6 +494,13 @@ object Datadog {
internal const val CANNOT_CREATE_SDK_INSTANCE_ID_ERROR =
"Cannot create SDK instance ID, stopping SDK initialization."

internal const val MISSING_DEPENDENCIES_ERROR =
"FlashCat SDK initialization failed because of missing dependencies: %s. " +
"Please make sure you have added them to your app's build.gradle file."

internal const val WARNING_WORKMANAGER_MISSING =
"WorkManager library not found. Background upload capabilities will be disabled."

internal const val DD_SOURCE_TAG = "_dd.source"
internal const val DD_SDK_VERSION_TAG = "_dd.sdk_version"
internal const val DD_APP_VERSION_TAG = "_dd.version"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.content.Context
import androidx.work.WorkManager
import com.datadog.android.api.InternalLogger
import com.datadog.android.core.internal.utils.cancelUploadWorker
import com.datadog.android.core.internal.utils.isInitialized
import com.datadog.android.core.internal.utils.triggerUploadWorker
import java.lang.ref.Reference
import java.lang.ref.WeakReference
Expand All @@ -25,7 +26,7 @@ internal class ProcessLifecycleCallback(

override fun onStarted() {
contextWeakRef.get()?.let {
if (WorkManager.isInitialized()) {
if (isInitialized(it)) {
cancelUploadWorker(it, instanceName, internalLogger)
}
}
Expand All @@ -37,7 +38,7 @@ internal class ProcessLifecycleCallback(

override fun onStopped() {
contextWeakRef.get()?.let {
if (WorkManager.isInitialized()) {
if (isInitialized(it)) {
triggerUploadWorker(it, instanceName, internalLogger)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package com.datadog.android.core.internal.utils

import android.content.Context
import android.util.Log
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
Expand All @@ -15,6 +16,7 @@ import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.datadog.android.api.InternalLogger
import com.datadog.android.core.UploadWorker
import java.lang.reflect.Method
import java.util.concurrent.TimeUnit

internal const val CANCEL_ERROR_MESSAGE = "Error cancelling the UploadWorker"
Expand Down Expand Up @@ -79,3 +81,75 @@ internal fun triggerUploadWorker(
)
}
}

private const val TAG = "FC_SDK_WorkManager"

private val isClassAvailable: Boolean by lazy {
try {
Class.forName("androidx.work.WorkManager")
true
} catch (e: Throwable) {
false
}
}

private val officialIsInitializedMethod: Method? by lazy {
if (!isClassAvailable) return@lazy null
try {
WorkManager::class.java.getMethod("isInitialized")
} catch (e: Throwable) {
null
}
}

@Volatile
private var isInitializedCache: Boolean? = null

/**
* 判断 WorkManager 是否已初始化。
* 兼容 WorkManager 2.8.0+ (反射调用官方方法) 和旧版本 (try-catch getInstance)。
* 安全应对 compileOnly 导致的类缺失。
*/
fun isInitialized(context: Context): Boolean {
isInitializedCache?.let { return it }

if (!isClassAvailable) {
return false.also { isInitializedCache = it }
}

officialIsInitializedMethod?.let { method ->
try {
val result = method.invoke(null) as Boolean
if (result) {
isInitializedCache = true
}
return result
} catch (e: Throwable) {
Log.w(TAG, "Failed to invoke isInitialized via reflection", e)
}
}

return try {
WorkManager.getInstance(context)
true.also { isInitializedCache = it }
} catch (e: IllegalStateException) {
false
} catch (e: Throwable) {
false
}
}

/**
* 安全获取实例,如果未初始化或库不存在则返回 null
*/
fun getWorkManagerOrNull(context: Context): WorkManager? {
return if (isInitialized(context)) {
try {
WorkManager.getInstance(context)
} catch (e: Throwable) {
null
}
} else {
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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.thread.waitToIdle
import com.datadog.android.core.internal.utils.isInitialized
import com.datadog.android.core.internal.utils.triggerUploadWorker
import com.datadog.android.internal.utils.asString
import com.datadog.android.internal.utils.loggableStackTrace
Expand Down Expand Up @@ -69,7 +70,7 @@ internal class DatadogExceptionHandler(

// trigger a task to send the logs ASAP
contextRef.get()?.let {
if (WorkManager.isInitialized()) {
if (isInitialized(it)) {
triggerUploadWorker(it, sdkCore.name, sdkCore.internalLogger)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
import org.mockito.stubbing.Answer

import com.datadog.android.utils.forge.WorkerParametersForgeryFactory

@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class),
Expand All @@ -74,7 +76,6 @@ internal class UploadWorkerTest {
@StringForgery
lateinit var fakeInstanceName: String

@Forgery
lateinit var fakeWorkerParameters: WorkerParameters

var fakeFeaturesCount: Int = 0
Expand All @@ -93,6 +94,8 @@ internal class UploadWorkerTest {

@BeforeEach
fun `set up`(forge: Forge) {
fakeWorkerParameters = WorkerParametersForgeryFactory().getForgery(forge)

whenever(mockSdkCore.getDatadogContext()) doReturn fakeDatadogContext
Datadog.registry.register(fakeInstanceName, mockSdkCore)

Expand Down Expand Up @@ -918,7 +921,6 @@ internal class UploadWorkerTest {
tags,
runtimeExtras,
runAttemptCount,
generation,
backgroundExecutor,
taskExecutor,
workerFactory,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal class Configurator :
// IO
forge.addFactory(BatchForgeryFactory())
forge.addFactory(PayloadDecorationForgeryFactory())
forge.addFactory(WorkerParametersForgeryFactory())
// forge.addFactory(WorkerParametersForgeryFactory())

// NDK Crash
forge.addFactory(NdkCrashLogForgeryFactory())
Expand Down
Loading
Loading