Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.Process
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
Expand Down Expand Up @@ -59,6 +60,7 @@ import com.datadog.android.core.internal.system.NoOpSystemInfoProvider
import com.datadog.android.core.internal.system.SystemInfoProvider
import com.datadog.android.core.internal.thread.BackPressureExecutorService
import com.datadog.android.core.internal.thread.BackPressuredBlockingQueue
import com.datadog.android.core.internal.thread.BroadcastReceiverThread
import com.datadog.android.core.internal.thread.DatadogThreadFactory
import com.datadog.android.core.internal.thread.LoggingScheduledThreadPoolExecutor
import com.datadog.android.core.internal.thread.ScheduledExecutorServiceFactory
Expand Down Expand Up @@ -201,6 +203,7 @@ internal class CoreFeature(
internal lateinit var uploadExecutorService: ScheduledThreadPoolExecutor
internal lateinit var persistenceExecutorService: FlushableExecutorService
internal lateinit var contextExecutorService: ThreadPoolExecutor
internal lateinit var broadcastReceiverThread: BroadcastReceiverThread
internal lateinit var backpressureStrategy: BackPressureStrategy

internal var localDataEncryption: Encryption? = null
Expand Down Expand Up @@ -589,15 +592,17 @@ internal class CoreFeature(
// Tracking Consent Provider
trackingConsentProvider = TrackingConsentProvider(consent)

val broadcastReceiverHandler = Handler(broadcastReceiverThread.looper)

// System Info Provider
systemInfoProvider = BroadcastReceiverSystemInfoProvider(
internalLogger = internalLogger,
executorService = contextExecutorService
handler = broadcastReceiverHandler
)
systemInfoProvider.register(appContext)

// Network Info Provider
setupNetworkInfoProviders(appContext)
setupNetworkInfoProviders(appContext, broadcastReceiverHandler)

// User Info Provider
userInfoProvider = DatadogUserInfoProvider()
Expand All @@ -606,7 +611,7 @@ internal class CoreFeature(
accountInfoProvider = DatadogAccountInfoProvider(internalLogger)
}

private fun setupNetworkInfoProviders(appContext: Context) {
private fun setupNetworkInfoProviders(appContext: Context, broadcastReceiverHandler: Handler) {
networkInfoProvider = if (buildSdkVersionProvider.isAtLeastN) {
CallbackNetworkInfoProvider(
internalLogger = internalLogger,
Expand All @@ -615,7 +620,7 @@ internal class CoreFeature(
} else {
BroadcastReceiverNetworkInfoProvider(
internalLogger = internalLogger,
executorService = contextExecutorService,
handler = broadcastReceiverHandler,
buildSdkVersionProvider = buildSdkVersionProvider
)
}
Expand Down Expand Up @@ -688,6 +693,9 @@ internal class CoreFeature(
contextQueue,
DatadogThreadFactory("context")
)
broadcastReceiverThread = BroadcastReceiverThread()
@Suppress("UnsafeThirdPartyFunctionCall") // constructed once; start() is called exactly once here
broadcastReceiverThread.start()
}

private fun resolveProcessInfo(appContext: Context) {
Expand Down Expand Up @@ -718,6 +726,7 @@ internal class CoreFeature(
uploadExecutorService.shutdownNow()
contextExecutorService.shutdownNow()
persistenceExecutorService.shutdownNow()
broadcastReceiverThread.quitSafely()

try {
uploadExecutorService.awaitTermination(1, TimeUnit.SECONDS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,20 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.Handler
import android.telephony.TelephonyManager
import com.datadog.android.api.InternalLogger
import com.datadog.android.api.context.NetworkInfo
import com.datadog.android.core.internal.receiver.ThreadSafeReceiver
import com.datadog.android.core.internal.utils.executeSafe
import com.datadog.android.internal.system.BuildSdkVersionProvider
import com.datadog.android.internal.utils.getSystemServiceAs
import java.util.concurrent.ExecutorService
import android.net.NetworkInfo as AndroidNetworkInfo

@Suppress("DEPRECATION")
@SuppressLint("InlinedApi")
internal class BroadcastReceiverNetworkInfoProvider(
private val internalLogger: InternalLogger,
private val executorService: ExecutorService,
private val handler: Handler,
private val buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT
) : ThreadSafeReceiver(),
NetworkInfoProvider {
Expand All @@ -38,26 +37,29 @@ internal class BroadcastReceiverNetworkInfoProvider(
// region BroadcastReceiver

override fun onReceive(context: Context, intent: Intent?) {
executorService.executeSafe(HANDLE_INTENT_OPERATION_NAME, internalLogger) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question about how ANR can happen with previous approach, since we offload immediately the onReceive from main thread to executorService, the blocking task which causes ANR must happen before onReceive is called?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. My guess is that PairIP instruments onReceive at the method entry point — before any code in the body runs, including the executorService.executeSafe(...) call. The overhead is in the PairIP VM's instrumentation of the method itself on the main thread looper, not in anything the body does. So by the time the executor would offload the work, the ANR-causing delay has already occurred on the main thread. Passing a background Handler to registerReceiver moves the entire dispatch — including the PairIP entry instrumentation — off the main thread, which is why I hope this approach will work.

handleIntent(context)
try {
val connectivityMgr = context.getSystemServiceAs<ConnectivityManager>(Context.CONNECTIVITY_SERVICE)
val activeNetworkInfo = connectivityMgr?.activeNetworkInfo

networkInfo = buildNetworkInfo(context, activeNetworkInfo)
} catch (@Suppress("TooGenericExceptionCaught") e: RuntimeException) {
internalLogger.log(
level = InternalLogger.Level.ERROR,
targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY),
messageBuilder = { ERROR_HANDLING_BROADCAST_INTENT },
throwable = e
)
}
}

private fun handleIntent(context: Context) {
val connectivityMgr = context.getSystemServiceAs<ConnectivityManager>(Context.CONNECTIVITY_SERVICE)
val activeNetworkInfo = connectivityMgr?.activeNetworkInfo

networkInfo = buildNetworkInfo(context, activeNetworkInfo)
}

// endregion

// region NetworkInfoProvider

override fun register(context: Context) {
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
registerReceiver(context, filter)
handleIntent(context)
registerReceiver(context, filter, handler)
onReceive(context, null)
}

override fun unregister(context: Context) {
Expand Down Expand Up @@ -149,10 +151,10 @@ internal class BroadcastReceiverNetworkInfoProvider(

companion object {

private const val HANDLE_INTENT_OPERATION_NAME = "BroadcastReceiverNetworkInfoProvider.handleIntent"

const val NETWORK_TYPE_LTE_CA = 19 // @Hide TelephonyManager.NETWORK_TYPE_LTE_CA,

private const val ERROR_HANDLING_BROADCAST_INTENT = "Error handling network info broadcast intent."

private val knownMobileTypes = setOf(
ConnectivityManager.TYPE_MOBILE,
ConnectivityManager.TYPE_MOBILE_DUN,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Handler
import com.datadog.android.internal.system.BuildSdkVersionProvider
import java.util.concurrent.atomic.AtomicBoolean

Expand All @@ -24,14 +25,15 @@ internal abstract class ThreadSafeReceiver(
@SuppressLint("WrongConstant", "UnspecifiedRegisterReceiverFlag")
fun registerReceiver(
context: Context,
filter: IntentFilter
filter: IntentFilter,
handler: Handler
): Intent? {
val intent = if (buildSdkVersionProvider.isAtLeastTiramisu) {
context.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED)
context.registerReceiver(this, filter, null, handler, Context.RECEIVER_NOT_EXPORTED)
} else if (buildSdkVersionProvider.isAtLeastO) {
context.registerReceiver(this, filter, RECEIVER_NOT_EXPORTED_COMPAT)
context.registerReceiver(this, filter, null, handler, RECEIVER_NOT_EXPORTED_COMPAT)
} else {
context.registerReceiver(this, filter)
context.registerReceiver(this, filter, null, handler)
}
isRegistered.set(true)
return intent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,16 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Handler
import android.os.PowerManager
import com.datadog.android.api.InternalLogger
import com.datadog.android.core.internal.receiver.ThreadSafeReceiver
import com.datadog.android.core.internal.utils.executeSafe
import com.datadog.android.internal.utils.getSystemServiceAs
import java.util.concurrent.ExecutorService
import kotlin.math.roundToInt

internal class BroadcastReceiverSystemInfoProvider(
private val internalLogger: InternalLogger,
private val executorService: ExecutorService
private val handler: Handler
) : ThreadSafeReceiver(), SystemInfoProvider {

@Volatile
Expand All @@ -30,12 +29,6 @@ internal class BroadcastReceiverSystemInfoProvider(
// region BroadcastReceiver

override fun onReceive(context: Context, intent: Intent?) {
executorService.executeSafe(HANDLE_INTENT_OPERATION_NAME, internalLogger) {
handleIntent(context, intent)
}
}

private fun handleIntent(context: Context, intent: Intent?) {
try {
when (val action = intent?.action) {
Intent.ACTION_BATTERY_CHANGED -> {
Expand Down Expand Up @@ -92,7 +85,7 @@ internal class BroadcastReceiverSystemInfoProvider(
private fun registerIntentFilter(context: Context, action: String) {
val filter = IntentFilter()
filter.addAction(action)
registerReceiver(context, filter)?.let { handleIntent(context, it) }
registerReceiver(context, filter, handler)?.let { onReceive(context, it) }
}

private fun handleBatteryIntent(intent: Intent) {
Expand Down Expand Up @@ -129,7 +122,6 @@ internal class BroadcastReceiverSystemInfoProvider(

companion object {

private const val HANDLE_INTENT_OPERATION_NAME = "BroadcastReceiverSystemInfoProvider.handleIntent"
private const val DEFAULT_BATTERY_SCALE = 100
private const val BATTERY_UNPLUGGED = -1
private const val BATTERY_LEVEL_UNKNOWN = -1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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.thread

import android.os.HandlerThread

/**
* A dedicated [HandlerThread] used as the dispatch thread for the SDK's
* [android.content.BroadcastReceiver]s. Passing a [android.os.Handler] built from this thread's
* looper to [android.content.Context.registerReceiver] ensures each `onReceive` callback is
* delivered on this background thread instead of the main thread, avoiding ANRs caused by
* per-call overhead on protected builds (e.g. PairIP).
*
* Lifecycle: start the thread before use and call [quitSafely] when the SDK is torn down.
*/
internal class BroadcastReceiverThread : HandlerThread("datadog-broadcast-receiver-thread")
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.datadog.android.core.internal.privacy.TrackingConsentProvider
import com.datadog.android.core.internal.system.BroadcastReceiverSystemInfoProvider
import com.datadog.android.core.internal.system.NoOpSystemInfoProvider
import com.datadog.android.core.internal.thread.BackPressuredBlockingQueue
import com.datadog.android.core.internal.thread.BroadcastReceiverThread
import com.datadog.android.core.internal.time.AppStartTimeProvider
import com.datadog.android.core.internal.time.KronosTimeProvider
import com.datadog.android.core.internal.user.DatadogUserInfoProvider
Expand Down Expand Up @@ -82,6 +83,7 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.inOrder
import org.mockito.kotlin.isA
import org.mockito.kotlin.isNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
Expand Down Expand Up @@ -231,7 +233,7 @@ internal class CoreFeatureTest {
// Then
argumentCaptor<BroadcastReceiver> {
verify(appContext.mockInstance, atLeastOnce())
.registerReceiver(capture(), any())
.registerReceiver(capture(), any(), isNull(), any())

assertThat(allValues)
.containsInstanceOf(BroadcastReceiverSystemInfoProvider::class.java)
Expand All @@ -256,7 +258,7 @@ internal class CoreFeatureTest {
// Then
argumentCaptor<BroadcastReceiver> {
verify(appContext.mockInstance, atLeastOnce())
.registerReceiver(capture(), any())
.registerReceiver(capture(), any(), isNull(), any())

assertThat(allValues)
.containsInstanceOf(BroadcastReceiverSystemInfoProvider::class.java)
Expand Down Expand Up @@ -1574,6 +1576,41 @@ internal class CoreFeatureTest {
}
}

@Test
fun `M create broadcastReceiverThread W initialize()`() {
// When
testedFeature.initialize(
appContext.mockInstance,
fakeSdkInstanceId,
fakeConfig,
fakeConsent
)

// Then — verify the wrapper is initialized (lateinit would throw if not)
assertThat(testedFeature.broadcastReceiverThread).isNotNull()
}

@Test
fun `M shutdown broadcastReceiverThread W stop()`() {
// Given
testedFeature.initialize(
appContext.mockInstance,
fakeSdkInstanceId,
fakeConfig,
fakeConsent
)
val mockBroadcastReceiverThread = mock<BroadcastReceiverThread>()
// Shut down the real thread before swapping in the mock to avoid leaking a HandlerThread
testedFeature.broadcastReceiverThread.quitSafely()
testedFeature.broadcastReceiverThread = mockBroadcastReceiverThread

// When
testedFeature.stop()

// Then
verify(mockBroadcastReceiverThread).quitSafely()
}

// endregion

// region createOkHttpCallFactory
Expand Down
Loading
Loading