From 530d28d570d4caabbc8cb631c0bf3aa7f12b2441 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 24 Jul 2026 08:01:55 +0200 Subject: [PATCH] fix(android): Scope device info cache to SDK options Store DeviceInfoUtil on each SentryAndroidOptions instance so repeated SDK initializations cannot reuse stale collection policy or Android services. Preserve lazy initialization while allowing old and new clients to retain their own device context. Refs #5666 Co-Authored-By: Claude --- .../api/sentry-android-core.api | 1 - .../sentry/android/core/DeviceInfoUtil.java | 23 +--------- .../android/core/SentryAndroidOptions.java | 16 +++++++ .../ApplicationExitInfoEventProcessorTest.kt | 1 - .../core/DefaultAndroidEventProcessorTest.kt | 1 - .../sentry/android/core/DeviceInfoUtilTest.kt | 42 ++++++++++++++++++- .../android/core/InternalSentrySdkTest.kt | 1 - 7 files changed, 58 insertions(+), 27 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index adebedf2700..2e965ca6e27 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -282,7 +282,6 @@ public final class io/sentry/android/core/DeviceInfoUtil { public fun getSplitApksInfo ()Lio/sentry/android/core/ContextUtils$SplitApksInfo; public fun getTotalMemory ()Ljava/lang/Long; public static fun isCharging (Landroid/content/Intent;Lio/sentry/SentryOptions;)Ljava/lang/Boolean; - public static fun resetInstance ()V } public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : io/sentry/Integration, java/io/Closeable { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java index d988cbd090e..397403ae9ad 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java @@ -15,7 +15,6 @@ import android.os.SystemClock; import android.util.DisplayMetrics; import io.sentry.DateUtils; -import io.sentry.ISentryLifecycleToken; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.CpuInfoUtils; @@ -23,7 +22,6 @@ import io.sentry.android.core.internal.util.RootChecker; import io.sentry.protocol.Device; import io.sentry.protocol.OperatingSystem; -import io.sentry.util.AutoClosableReentrantLock; import java.io.File; import java.util.Calendar; import java.util.Collections; @@ -34,17 +32,10 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; @ApiStatus.Internal public final class DeviceInfoUtil { - @SuppressLint("StaticFieldLeak") - private static volatile DeviceInfoUtil instance; - - private static final @NotNull AutoClosableReentrantLock staticLock = - new AutoClosableReentrantLock(); - private final @NotNull Context context; private final @NotNull SentryAndroidOptions options; private final @NotNull BuildInfoProvider buildInfoProvider; @@ -80,19 +71,7 @@ public DeviceInfoUtil( @NotNull public static DeviceInfoUtil getInstance( final @NotNull Context context, final @NotNull SentryAndroidOptions options) { - if (instance == null) { - try (final @NotNull ISentryLifecycleToken ignored = staticLock.acquire()) { - if (instance == null) { - instance = new DeviceInfoUtil(ContextUtils.getApplicationContext(context), options); - } - } - } - return instance; - } - - @TestOnly - public static void resetInstance() { - instance = null; + return options.getOrCreateDeviceInfoUtil(context); } // we can get some inspiration here diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index 615db97a28d..1a4f8483af8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -3,8 +3,10 @@ import android.app.Activity; import android.app.ActivityManager; import android.app.ApplicationExitInfo; +import android.content.Context; import io.sentry.Hint; import io.sentry.IScope; +import io.sentry.ISentryLifecycleToken; import io.sentry.ISpan; import io.sentry.Sentry; import io.sentry.SentryEvent; @@ -141,6 +143,8 @@ public final class SentryAndroidOptions extends SentryOptions { /** Enables or disables collecting of external storage context. */ private boolean collectExternalStorageContext = false; + private volatile @Nullable DeviceInfoUtil deviceInfoUtil; + /** * Controls how many seconds to wait for sending events in case there were Startup Crashes in the * previous run. Sentry SDKs normally send events from a background queue, but in the case of @@ -200,6 +204,18 @@ public final class SentryAndroidOptions extends SentryOptions { /** Enable or disable intent extras reporting for system event breadcrumbs. Default is false. */ private boolean enableSystemEventBreadcrumbsExtras = false; + @NotNull + DeviceInfoUtil getOrCreateDeviceInfoUtil(final @NotNull Context context) { + if (deviceInfoUtil == null) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (deviceInfoUtil == null) { + deviceInfoUtil = new DeviceInfoUtil(ContextUtils.getApplicationContext(context), this); + } + } + } + return deviceInfoUtil; + } + public interface BeforeCaptureCallback { /** diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt index 7eaa269f39f..012013cafd5 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt @@ -197,7 +197,6 @@ class ApplicationExitInfoEventProcessorTest { @BeforeTest fun `set up`() { - DeviceInfoUtil.resetInstance() fixture.context = ApplicationProvider.getApplicationContext() } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt index fbcd20b99fb..eab6ceacc13 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt @@ -87,7 +87,6 @@ class DefaultAndroidEventProcessorTest { fun `set up`() { context = ApplicationProvider.getApplicationContext() AppState.getInstance().resetInstance() - DeviceInfoUtil.resetInstance() CpuInfoUtils.getInstance().clear() } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DeviceInfoUtilTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DeviceInfoUtilTest.kt index 49c828b551e..3cd9e079da9 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/DeviceInfoUtilTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/DeviceInfoUtilTest.kt @@ -15,7 +15,9 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertNotSame import kotlin.test.assertNull +import kotlin.test.assertSame import org.junit.runner.RunWith import org.robolectric.annotation.Config @@ -32,7 +34,45 @@ class DeviceInfoUtilTest { .putExtra(BatteryManager.EXTRA_LEVEL, 75) .putExtra(BatteryManager.EXTRA_PLUGGED, 0) ) - DeviceInfoUtil.resetInstance() + } + + @Test + fun `same options reuse device info util`() { + val options = SentryAndroidOptions() + + val first = DeviceInfoUtil.getInstance(context, options) + val second = DeviceInfoUtil.getInstance(context, options) + + assertSame(first, second) + } + + @Test + fun `different options use isolated device info utils`() { + val enabledOptions = + SentryAndroidOptions().apply { + dataCollection.setUserInfo(true) + isCollectAdditionalContext = true + isEnableRootCheck = true + } + val disabledOptions = + SentryAndroidOptions().apply { + dataCollection.setUserInfo(false) + isCollectAdditionalContext = false + isEnableRootCheck = false + } + + val enabled = DeviceInfoUtil.getInstance(context, enabledOptions) + val disabled = DeviceInfoUtil.getInstance(context, disabledOptions) + val enabledDevice = enabled.collectDeviceInformation(true, false) + val disabledDevice = disabled.collectDeviceInformation(true, false) + + assertNotSame(enabled, disabled) + assertNotNull(enabledDevice.id) + assertNotNull(enabledDevice.storageSize) + assertNotNull(enabled.operatingSystem.isRooted) + assertNull(disabledDevice.id) + assertNull(disabledDevice.storageSize) + assertNull(disabled.operatingSystem.isRooted) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt index 8c552a8b633..bd689c6a453 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt @@ -231,7 +231,6 @@ class InternalSentrySdkTest { fun `set up`() { Sentry.close() context = ApplicationProvider.getApplicationContext() - DeviceInfoUtil.resetInstance() } @Test