From 3596b1c92cc9fba787661ac645b84135e236e8bd Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 29 Aug 2024 11:30:08 +0200 Subject: [PATCH 1/4] Ensure app context is used even when SDK is initialized via Activity Context --- buildSrc/src/main/java/Config.kt | 3 ++- .../api/sentry-android-core.api | 1 + .../core/AndroidOptionsInitializer.java | 5 +---- .../core/AndroidTransactionProfiler.java | 4 +++- .../io/sentry/android/core/AnrIntegration.java | 2 +- .../android/core/AnrV2EventProcessor.java | 2 +- .../sentry/android/core/AnrV2Integration.java | 2 +- .../AppComponentsBreadcrumbsIntegration.java | 3 ++- .../io/sentry/android/core/ContextUtils.java | 15 +++++++++++++++ .../core/DefaultAndroidEventProcessor.java | 6 ++++-- .../io/sentry/android/core/DeviceInfoUtil.java | 2 +- .../core/NetworkBreadcrumbsIntegration.java | 3 ++- .../core/PhoneStateBreadcrumbsIntegration.java | 3 ++- .../core/SentryPerformanceProvider.java | 5 ++--- .../SystemEventsBreadcrumbsIntegration.java | 3 ++- .../core/TempSensorBreadcrumbsIntegration.java | 3 ++- .../debugmeta/AssetsDebugMetaLoader.java | 3 ++- .../internal/modules/AssetsModulesLoader.java | 3 ++- .../util/AndroidConnectionStatusProvider.java | 3 ++- .../util/SentryFrameMetricsCollector.java | 9 ++++++--- .../io/sentry/android/core/ContextUtilsTest.kt | 18 ++++++++++++++++++ .../sentry/android/core/SentryAndroidTest.kt | 2 +- .../sentry-uitest-android/build.gradle.kts | 2 ++ .../io/sentry/uitest/android/BaseUiTest.kt | 1 + .../io/sentry/uitest/android/SdkInitTests.kt | 15 +++++++++++++++ .../sentry/android/replay/ReplayIntegration.kt | 5 +++-- .../io/sentry/android/replay/util/Context.kt | 5 +++++ 27 files changed, 99 insertions(+), 29 deletions(-) create mode 100644 sentry-android-replay/src/main/java/io/sentry/android/replay/util/Context.kt diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 8777d926a9f..f74fcb4953d 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -53,7 +53,7 @@ object Config { val appCompat = "androidx.appcompat:appcompat:1.3.0" val timber = "com.jakewharton.timber:timber:4.7.1" val okhttp = "com.squareup.okhttp3:okhttp:$okHttpVersion" - val leakCanary = "com.squareup.leakcanary:leakcanary-android:2.8.1" + val leakCanary = "com.squareup.leakcanary:leakcanary-android:2.14" val constraintLayout = "androidx.constraintlayout:constraintlayout:2.1.3" private val lifecycleVersion = "2.2.0" @@ -197,6 +197,7 @@ object Config { val hsqldb = "org.hsqldb:hsqldb:2.6.1" val javaFaker = "com.github.javafaker:javafaker:1.0.2" val msgpack = "org.msgpack:msgpack-core:0.9.8" + val leakCanaryInstrumentation = "com.squareup.leakcanary:leakcanary-android-instrumentation:2.14" } object QualityPlugins { diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 478a1ddd3ce..f525e056f6c 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -158,6 +158,7 @@ public final class io/sentry/android/core/BuildInfoProvider { } public final class io/sentry/android/core/ContextUtils { + public static fun getApplicationContext (Landroid/content/Context;)Landroid/content/Context; public static fun isForegroundImportance ()Z } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 2d559fd7817..d5dfce77b28 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -90,10 +90,7 @@ static void loadDefaultAndMetadataOptions( final @NotNull BuildInfoProvider buildInfoProvider) { Objects.requireNonNull(context, "The context is required."); - // it returns null if ContextImpl, so let's check for nullability - if (context.getApplicationContext() != null) { - context = context.getApplicationContext(); - } + context = ContextUtils.getApplicationContext(context); Objects.requireNonNull(options, "The options object is required."); Objects.requireNonNull(logger, "The ILogger object is required."); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java index d9ece7fb464..41e57a886a4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java @@ -87,7 +87,9 @@ public AndroidTransactionProfiler( final boolean isProfilingEnabled, final int profilingTracesHz, final @NotNull ISentryExecutorService executorService) { - this.context = Objects.requireNonNull(context, "The application context is required"); + this.context = + Objects.requireNonNull( + ContextUtils.getApplicationContext(context), "The application context is required"); this.logger = Objects.requireNonNull(logger, "ILogger is required"); this.frameMetricsCollector = Objects.requireNonNull(frameMetricsCollector, "SentryFrameMetricsCollector is required"); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java index 0ad2c242da3..1c7b0f2eaf2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java @@ -33,7 +33,7 @@ public final class AnrIntegration implements Integration, Closeable { private final @NotNull Object startLock = new Object(); public AnrIntegration(final @NotNull Context context) { - this.context = context; + this.context = ContextUtils.getApplicationContext(context); } /** diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java index b1751d5cc81..d58d04b7f81 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -97,7 +97,7 @@ public AnrV2EventProcessor( final @NotNull SentryAndroidOptions options, final @NotNull BuildInfoProvider buildInfoProvider, final @Nullable SecureRandom random) { - this.context = context; + this.context = ContextUtils.getApplicationContext(context); this.options = options; this.buildInfoProvider = buildInfoProvider; this.random = random; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 669233bb09a..b6be55e90ac 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -63,7 +63,7 @@ public AnrV2Integration(final @NotNull Context context) { AnrV2Integration( final @NotNull Context context, final @NotNull ICurrentDateProvider dateProvider) { - this.context = context; + this.context = ContextUtils.getApplicationContext(context); this.dateProvider = dateProvider; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java index eef47076837..97c7d06ce7d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java @@ -29,7 +29,8 @@ public final class AppComponentsBreadcrumbsIntegration private @Nullable SentryAndroidOptions options; public AppComponentsBreadcrumbsIntegration(final @NotNull Context context) { - this.context = Objects.requireNonNull(context, "Context is required"); + this.context = + Objects.requireNonNull(ContextUtils.getApplicationContext(context), "Context is required"); } @Override diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java index 2e76de4d123..89fe856631b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java @@ -383,4 +383,19 @@ static void setAppPackageInfo( } app.setPermissions(permissions); } + + /** + * Get the app context + * + * @return the app context, or if not available, the provided context + */ + @NotNull + public static Context getApplicationContext(final @NotNull Context context) { + // it returns null if ContextImpl, so let's check for nullability + final @Nullable Context appContext = context.getApplicationContext(); + if (appContext != null) { + return appContext; + } + return context; + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 5ef35cbfe1d..a2833d2b346 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -47,7 +47,9 @@ public DefaultAndroidEventProcessor( final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull SentryAndroidOptions options) { - this.context = Objects.requireNonNull(context, "The application context is required."); + this.context = + Objects.requireNonNull( + ContextUtils.getApplicationContext(context), "The application context is required."); this.buildInfoProvider = Objects.requireNonNull(buildInfoProvider, "The BuildInfoProvider is required."); this.options = Objects.requireNonNull(options, "The options object is required."); @@ -57,7 +59,7 @@ public DefaultAndroidEventProcessor( // some device info performs disk I/O, but it's result is cached, let's pre-cache it final @NotNull ExecutorService executorService = Executors.newSingleThreadExecutor(); this.deviceInfoUtil = - executorService.submit(() -> DeviceInfoUtil.getInstance(context, options)); + executorService.submit(() -> DeviceInfoUtil.getInstance(this.context, options)); executorService.shutdown(); } 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 f1debc5d238..e2dfee2705a 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 @@ -76,7 +76,7 @@ public static DeviceInfoUtil getInstance( if (instance == null) { synchronized (DeviceInfoUtil.class) { if (instance == null) { - instance = new DeviceInfoUtil(context.getApplicationContext(), options); + instance = new DeviceInfoUtil(ContextUtils.getApplicationContext(context), options); } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java index 1cd42e9dab9..e30dfb681c4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java @@ -42,7 +42,8 @@ public NetworkBreadcrumbsIntegration( final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull ILogger logger) { - this.context = Objects.requireNonNull(context, "Context is required"); + this.context = + Objects.requireNonNull(ContextUtils.getApplicationContext(context), "Context is required"); this.buildInfoProvider = Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required"); this.logger = Objects.requireNonNull(logger, "ILogger is required"); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java index c10d25b0579..2da0452698b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java @@ -28,7 +28,8 @@ public final class PhoneStateBreadcrumbsIntegration implements Integration, Clos private final @NotNull Object startLock = new Object(); public PhoneStateBreadcrumbsIntegration(final @NotNull Context context) { - this.context = Objects.requireNonNull(context, "Context is required"); + this.context = + Objects.requireNonNull(ContextUtils.getApplicationContext(context), "Context is required"); } @Override diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 2ad465f1e3f..971ead378ff 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -159,10 +159,9 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri final @NotNull ITransactionProfiler appStartProfiler = new AndroidTransactionProfiler( - context.getApplicationContext(), + context, buildInfoProvider, - new SentryFrameMetricsCollector( - context.getApplicationContext(), logger, buildInfoProvider), + new SentryFrameMetricsCollector(context, logger, buildInfoProvider), logger, profilingOptions.getProfilingTracesDirPath(), profilingOptions.isProfilingEnabled(), diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java index dcd92e8bf88..f196b7ca90a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java @@ -77,7 +77,8 @@ public SystemEventsBreadcrumbsIntegration(final @NotNull Context context) { public SystemEventsBreadcrumbsIntegration( final @NotNull Context context, final @NotNull List actions) { - this.context = Objects.requireNonNull(context, "Context is required"); + this.context = + Objects.requireNonNull(ContextUtils.getApplicationContext(context), "Context is required"); this.actions = Objects.requireNonNull(actions, "Actions list is required"); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java index eaf5c64991b..41e18601840 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java @@ -34,7 +34,8 @@ public final class TempSensorBreadcrumbsIntegration private final @NotNull Object startLock = new Object(); public TempSensorBreadcrumbsIntegration(final @NotNull Context context) { - this.context = Objects.requireNonNull(context, "Context is required"); + this.context = + Objects.requireNonNull(ContextUtils.getApplicationContext(context), "Context is required"); } @Override diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/debugmeta/AssetsDebugMetaLoader.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/debugmeta/AssetsDebugMetaLoader.java index 6c5bed5ae2a..568b67f0b02 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/debugmeta/AssetsDebugMetaLoader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/debugmeta/AssetsDebugMetaLoader.java @@ -6,6 +6,7 @@ import android.content.res.AssetManager; import io.sentry.ILogger; import io.sentry.SentryLevel; +import io.sentry.android.core.ContextUtils; import io.sentry.internal.debugmeta.IDebugMetaLoader; import java.io.BufferedInputStream; import java.io.FileNotFoundException; @@ -24,7 +25,7 @@ public final class AssetsDebugMetaLoader implements IDebugMetaLoader { private final @NotNull ILogger logger; public AssetsDebugMetaLoader(final @NotNull Context context, final @NotNull ILogger logger) { - this.context = context; + this.context = ContextUtils.getApplicationContext(context); this.logger = logger; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/modules/AssetsModulesLoader.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/modules/AssetsModulesLoader.java index 6d6f3737cba..b6374a32e36 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/modules/AssetsModulesLoader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/modules/AssetsModulesLoader.java @@ -3,6 +3,7 @@ import android.content.Context; import io.sentry.ILogger; import io.sentry.SentryLevel; +import io.sentry.android.core.ContextUtils; import io.sentry.internal.modules.ModulesLoader; import java.io.FileNotFoundException; import java.io.IOException; @@ -19,7 +20,7 @@ public final class AssetsModulesLoader extends ModulesLoader { public AssetsModulesLoader(final @NotNull Context context, final @NotNull ILogger logger) { super(logger); - this.context = context; + this.context = ContextUtils.getApplicationContext(context); } @Override diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index b8279edcb1f..0afd2bce970 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -12,6 +12,7 @@ import io.sentry.ILogger; import io.sentry.SentryLevel; import io.sentry.android.core.BuildInfoProvider; +import io.sentry.android.core.ContextUtils; import java.util.HashMap; import java.util.Map; import org.jetbrains.annotations.ApiStatus; @@ -37,7 +38,7 @@ public AndroidConnectionStatusProvider( @NotNull Context context, @NotNull ILogger logger, @NotNull BuildInfoProvider buildInfoProvider) { - this.context = context; + this.context = ContextUtils.getApplicationContext(context); this.logger = logger; this.buildInfoProvider = buildInfoProvider; this.registeredCallbacks = new HashMap<>(); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java index 27731e48cff..25ff5da2bdb 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java @@ -17,6 +17,7 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.BuildInfoProvider; +import io.sentry.android.core.ContextUtils; import io.sentry.util.Objects; import java.lang.ref.WeakReference; import java.lang.reflect.Field; @@ -84,7 +85,9 @@ public SentryFrameMetricsCollector( final @NotNull ILogger logger, final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull WindowFrameMetricsManager windowFrameMetricsManager) { - Objects.requireNonNull(context, "The context is required"); + final @NotNull Context appContext = + Objects.requireNonNull( + ContextUtils.getApplicationContext(context), "The context is required"); this.logger = Objects.requireNonNull(logger, "Logger is required"); this.buildInfoProvider = Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required"); @@ -92,7 +95,7 @@ public SentryFrameMetricsCollector( Objects.requireNonNull(windowFrameMetricsManager, "WindowFrameMetricsManager is required"); // registerActivityLifecycleCallbacks is only available if Context is an AppContext - if (!(context instanceof Application)) { + if (!(appContext instanceof Application)) { return; } // FrameMetrics api is only available since sdk version N @@ -110,7 +113,7 @@ public SentryFrameMetricsCollector( // We have to register the lifecycle callback, even if no profile is started, otherwise when we // start a profile, we wouldn't have the current activity and couldn't get the frameMetrics. - ((Application) context).registerActivityLifecycleCallbacks(this); + ((Application) appContext).registerActivityLifecycleCallbacks(this); // Most considerations regarding timestamps of frames are inspired from JankStats library: // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi24Impl.kt diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt index b758fae1f83..588a32a6569 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt @@ -29,6 +29,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertSame import kotlin.test.assertTrue @Config(sdk = [33]) @@ -213,4 +214,21 @@ class ContextUtilsTest { ) assertFalse(ContextUtils.isForegroundImportance()) } + + @Test + fun `getApplicationContext returns context if app context is null`() { + val contextMock = mock() + val appContext = ContextUtils.getApplicationContext(contextMock) + assertSame(contextMock, appContext) + } + + @Test + fun `getApplicationContext returns app context`() { + val contextMock = mock() + val appContextMock = mock() + whenever(contextMock.applicationContext).thenReturn(appContextMock) + + val appContext = ContextUtils.getApplicationContext(contextMock) + assertSame(appContextMock, appContext) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index aa721bdb413..9aa008e9cfd 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -362,7 +362,7 @@ class SentryAndroidTest { optionsConfig: (SentryAndroidOptions) -> Unit = {}, callback: (session: Session?) -> Unit ) { - Mockito.mockStatic(ContextUtils::class.java).use { mockedContextUtils -> + Mockito.mockStatic(ContextUtils::class.java, Mockito.CALLS_REAL_METHODS).use { mockedContextUtils -> mockedContextUtils.`when` { ContextUtils.isForegroundImportance() } .thenReturn(inForeground) SentryAndroid.init(context) { options -> diff --git a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts index 98b00eb4f80..7755166a4e2 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts @@ -109,6 +109,7 @@ dependencies { implementation(Config.Libs.androidxRecylerView) implementation(Config.Libs.constraintLayout) implementation(Config.TestLibs.espressoIdlingResource) + implementation(Config.Libs.leakCanary) compileOnly(Config.CompileOnly.nopen) errorprone(Config.CompileOnly.nopenChecker) @@ -123,6 +124,7 @@ dependencies { androidTestImplementation(Config.TestLibs.androidxTestCoreKtx) androidTestImplementation(Config.TestLibs.mockWebserver) androidTestImplementation(Config.TestLibs.androidxJunit) + androidTestImplementation(Config.TestLibs.leakCanaryInstrumentation) androidTestUtil(Config.TestLibs.androidxTestOrchestrator) } diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/BaseUiTest.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/BaseUiTest.kt index d6d3d1b6662..bfac0c3b5f2 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/BaseUiTest.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/BaseUiTest.kt @@ -77,6 +77,7 @@ abstract class BaseUiTest { */ protected fun initSentry( relayWaitForRequests: Boolean = false, + context: Context = this.context, optionsConfiguration: ((options: SentryAndroidOptions) -> Unit)? = null ) { relay.waitForRequests = relayWaitForRequests diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt index c942783548b..3dca87942b6 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt @@ -9,6 +9,7 @@ import io.sentry.android.core.AndroidLogger import io.sentry.android.core.SentryAndroidOptions import io.sentry.assertEnvelopeTransaction import io.sentry.protocol.SentryTransaction +import leakcanary.LeakAssertions import org.junit.runner.RunWith import kotlin.test.Test import kotlin.test.assertEquals @@ -154,4 +155,18 @@ class SdkInitTests : BaseUiTest() { val restartMs = afterRestart - beforeRestart assertTrue(restartMs > 3000, "Expected more than 3000 ms for SDK close and restart. Got $restartMs ms") } + + @Test + fun initViaActivityDoesNotLeak() { + val activityScenario = launchActivity() + activityScenario.moveToState(Lifecycle.State.RESUMED) + + activityScenario.onActivity { activity -> + initSentry(context = activity) + } + + activityScenario.moveToState(Lifecycle.State.DESTROYED) + + LeakAssertions.assertNoLeaks() + } } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index 996f7d2ba4c..455a996450a 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -23,6 +23,7 @@ import io.sentry.android.replay.capture.SessionCaptureStrategy import io.sentry.android.replay.gestures.GestureRecorder import io.sentry.android.replay.gestures.TouchRecorderCallback import io.sentry.android.replay.util.MainLooperHandler +import io.sentry.android.replay.util.appContext import io.sentry.android.replay.util.sample import io.sentry.android.replay.util.submitSafely import io.sentry.cache.PersistingScopeObserver @@ -51,7 +52,7 @@ public class ReplayIntegration( // needed for the Java's call site constructor(context: Context, dateProvider: ICurrentDateProvider) : this( - context, + context.appContext(), dateProvider, null, null, @@ -67,7 +68,7 @@ public class ReplayIntegration( replayCaptureStrategyProvider: ((isFullSession: Boolean) -> CaptureStrategy)? = null, mainLooperHandler: MainLooperHandler? = null, gestureRecorderProvider: (() -> GestureRecorder)? = null - ) : this(context, dateProvider, recorderProvider, recorderConfigProvider, replayCacheProvider) { + ) : this(context.appContext(), dateProvider, recorderProvider, recorderConfigProvider, replayCacheProvider) { this.replayCaptureStrategyProvider = replayCaptureStrategyProvider this.mainLooperHandler = mainLooperHandler ?: MainLooperHandler() this.gestureRecorderProvider = gestureRecorderProvider diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Context.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Context.kt new file mode 100644 index 00000000000..3c5be33115f --- /dev/null +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Context.kt @@ -0,0 +1,5 @@ +package io.sentry.android.replay.util + +import android.content.Context + +internal fun Context.appContext() = this.applicationContext ?: this From b8ac1a1482c86a255109d52542dbd9db1e83bd0e Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 29 Aug 2024 12:17:18 +0200 Subject: [PATCH 2/4] Update Changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d26f36b9703..6eb67528d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Fix ensure Application Context is used even when SDK is initialized via Activity Context ([#3669](https://github.com/getsentry/sentry-java/pull/3669)) + ## 7.14.0 ### Features From 5b73955058c3538e0968a6e2fa14feb83991ad81 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 29 Aug 2024 14:38:55 +0200 Subject: [PATCH 3/4] Exclude saucelabs from leakcanary --- .../io/sentry/uitest/android/SdkInitTests.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt index 3dca87942b6..a34ee207428 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt @@ -10,7 +10,11 @@ import io.sentry.android.core.SentryAndroidOptions import io.sentry.assertEnvelopeTransaction import io.sentry.protocol.SentryTransaction import leakcanary.LeakAssertions +import leakcanary.LeakCanary import org.junit.runner.RunWith +import shark.AndroidReferenceMatchers +import shark.IgnoredReferenceMatcher +import shark.ReferencePattern import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -158,6 +162,18 @@ class SdkInitTests : BaseUiTest() { @Test fun initViaActivityDoesNotLeak() { + LeakCanary.config = LeakCanary.config.copy( + referenceMatchers = AndroidReferenceMatchers.appDefaults + + listOf( + IgnoredReferenceMatcher( + ReferencePattern.InstanceFieldPattern( + "com.saucelabs.rdcinjector.testfairy.TestFairyEventQueue", + "context" + ) + ) + ) + ) + val activityScenario = launchActivity() activityScenario.moveToState(Lifecycle.State.RESUMED) From 24483cb0b5558877e4ae3c6829a5bd403dc8c618 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 29 Aug 2024 15:33:15 +0200 Subject: [PATCH 4/4] Exclude saucelabs TouchListener from leakCanary --- .../io/sentry/uitest/android/SdkInitTests.kt | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt index a34ee207428..cd7c60ac94c 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt @@ -68,7 +68,10 @@ class SdkInitTests : BaseUiTest() { relay.assert { findEnvelope { - assertEnvelopeTransaction(it.items.toList(), AndroidLogger()).transaction == "e2etests2" + assertEnvelopeTransaction( + it.items.toList(), + AndroidLogger() + ).transaction == "e2etests2" }.assert { val transactionItem: SentryTransaction = it.assertTransaction() // Profiling uses executorService, so if the executorService is shutdown it would fail @@ -110,7 +113,10 @@ class SdkInitTests : BaseUiTest() { Sentry.startTransaction("afterRestart", "emptyTransaction").finish() // We assert for less than 1 second just to account for slow devices in saucelabs or headless emulator - assertTrue(restartMs < 1000, "Expected less than 1000 ms for SDK restart. Got $restartMs ms") + assertTrue( + restartMs < 1000, + "Expected less than 1000 ms for SDK restart. Got $restartMs ms" + ) relay.assert { findEnvelope { @@ -157,7 +163,10 @@ class SdkInitTests : BaseUiTest() { } val afterRestart = System.currentTimeMillis() val restartMs = afterRestart - beforeRestart - assertTrue(restartMs > 3000, "Expected more than 3000 ms for SDK close and restart. Got $restartMs ms") + assertTrue( + restartMs > 3000, + "Expected more than 3000 ms for SDK close and restart. Got $restartMs ms" + ) } @Test @@ -170,6 +179,12 @@ class SdkInitTests : BaseUiTest() { "com.saucelabs.rdcinjector.testfairy.TestFairyEventQueue", "context" ) + ), + IgnoredReferenceMatcher( + ReferencePattern.StaticFieldPattern( + "com.testfairy.modules.capture.TouchListener", + "k" + ) ) ) )