From 3c4324c46dec7ceefc89a34744cfbfdb2d67a408 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 13 Oct 2025 15:38:10 +0200 Subject: [PATCH 001/546] Add sentry-android-distribution module to release configuration (#4791) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- .craft.yml | 1 + README.md | 1 + build.gradle.kts | 2 +- sentry-android-distribution/README.md | 3 +++ 4 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 sentry-android-distribution/README.md diff --git a/.craft.yml b/.craft.yml index 7dbf0382589..cf59692fc94 100644 --- a/.craft.yml +++ b/.craft.yml @@ -34,6 +34,7 @@ targets: maven:io.sentry:sentry-apache-http-client-5: maven:io.sentry:sentry-android: maven:io.sentry:sentry-android-core: + maven:io.sentry:sentry-android-distribution: maven:io.sentry:sentry-android-ndk: maven:io.sentry:sentry-android-timber: maven:io.sentry:sentry-kotlin-extensions: diff --git a/README.md b/README.md index 096ecf7d19a..edd447a8013 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Sentry SDK for Java and Android |-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------- | | sentry-android | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android) | 21 | | sentry-android-core | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core) | 21 | +| sentry-android-distribution | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-distribution/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-distribution) | 21 | | sentry-android-ndk | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk) | 21 | | sentry-android-timber | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber) | 21 | | sentry-android-fragment | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment) | 21 | diff --git a/build.gradle.kts b/build.gradle.kts index 23eaca36936..17b57291eb2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -161,7 +161,7 @@ subprojects { } } - if (!this.name.contains("sample") && !this.name.contains("integration-tests") && this.name != "sentry-system-test-support" && this.name != "sentry-test-support" && this.name != "sentry-android-distribution") { + if (!this.name.contains("sample") && !this.name.contains("integration-tests") && this.name != "sentry-system-test-support" && this.name != "sentry-test-support") { apply() apply() diff --git a/sentry-android-distribution/README.md b/sentry-android-distribution/README.md new file mode 100644 index 00000000000..ccfa938e88f --- /dev/null +++ b/sentry-android-distribution/README.md @@ -0,0 +1,3 @@ +# sentry-android-distribution + +This module contains the client library for the Sentry Android Build Distribution that checks for updates for your application automatically for internal testing purposes. From e881baebb0a26ffff1feef617ea3de77d1566d46 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 13 Oct 2025 16:54:50 +0200 Subject: [PATCH 002/546] docs(changelog): Add Sentry Android Distribution module entry (#4804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(changelog): Add Sentry Android Distribution module entry Add changelog entry for the experimental Sentry Android Distribution module that integrates with Sentry Build Distribution to check for and install updates. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * docs(changelog): Add PR link to Android Distribution entry ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cb73a5552f..a0307d0e0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add experimental Sentry Android Distribution module for integrating with Sentry Build Distribution to check for and install updates ([#4804](https://github.com/getsentry/sentry-java/pull/4804)) + ### Fixes - Use logger from options for JVM profiler ([#4771](https://github.com/getsentry/sentry-java/pull/4771)) From cf708bd2c180f26ddbfd25ca36bf5d3488da3046 Mon Sep 17 00:00:00 2001 From: Stefano Date: Tue, 14 Oct 2025 11:16:49 +0200 Subject: [PATCH 003/546] Avoid StrictMode warnings (#4724) * Removed few useless IO calls * Added SentryOptions.runtimeManager to run methods with Stricmode LAX policies --- CHANGELOG.md | 1 + .../core/AndroidOptionsInitializer.java | 16 ++-- .../core/SentryPerformanceProvider.java | 6 +- .../core/cache/AndroidEnvelopeCache.java | 17 ++-- .../internal/util/AndroidRuntimeManager.java | 23 +++++ .../core/internal/util/CpuInfoUtils.java | 2 - .../core/AndroidOptionsInitializerTest.kt | 7 ++ .../util/AndroidRuntimeManagerTest.kt | 87 +++++++++++++++++++ .../io/sentry/uitest/android/SdkInitTests.kt | 10 +++ sentry/api/sentry.api | 15 ++++ .../java/io/sentry/DirectoryProcessor.java | 24 ++--- sentry/src/main/java/io/sentry/Sentry.java | 9 +- .../main/java/io/sentry/SentryOptions.java | 25 ++++++ .../java/io/sentry/cache/EnvelopeCache.java | 15 ++-- .../sentry/util/runtime/IRuntimeManager.java | 13 +++ .../util/runtime/NeutralRuntimeManager.java | 12 +++ .../test/java/io/sentry/EnvelopeSenderTest.kt | 11 +-- .../util/runtime/NeutralRuntimeManagerTest.kt | 19 ++++ 18 files changed, 261 insertions(+), 51 deletions(-) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidRuntimeManager.java create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidRuntimeManagerTest.kt create mode 100644 sentry/src/main/java/io/sentry/util/runtime/IRuntimeManager.java create mode 100644 sentry/src/main/java/io/sentry/util/runtime/NeutralRuntimeManager.java create mode 100644 sentry/src/test/java/io/sentry/util/runtime/NeutralRuntimeManagerTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index a0307d0e0e6..570cd9cc00a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixes +- Avoid StrictMode warnings ([#4724](https://github.com/getsentry/sentry-java/pull/4724)) - Use logger from options for JVM profiler ([#4771](https://github.com/getsentry/sentry-java/pull/4771)) - Session Replay: Avoid deadlock when pausing replay if no connection ([#4788](https://github.com/getsentry/sentry-java/pull/4788)) 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 70c76c72824..02006d0924b 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 @@ -30,6 +30,7 @@ import io.sentry.android.core.internal.modules.AssetsModulesLoader; import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider; import io.sentry.android.core.internal.util.AndroidCurrentDateProvider; +import io.sentry.android.core.internal.util.AndroidRuntimeManager; import io.sentry.android.core.internal.util.AndroidThreadChecker; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.core.performance.AppStartMetrics; @@ -108,7 +109,7 @@ static void loadDefaultAndMetadataOptions( final @NotNull BuildInfoProvider buildInfoProvider) { Objects.requireNonNull(context, "The context is required."); - context = ContextUtils.getApplicationContext(context); + @NotNull final Context finalContext = ContextUtils.getApplicationContext(context); Objects.requireNonNull(options, "The options object is required."); Objects.requireNonNull(logger, "The ILogger object is required."); @@ -120,17 +121,22 @@ static void loadDefaultAndMetadataOptions( options.setDefaultScopeType(ScopeType.CURRENT); options.setOpenTelemetryMode(SentryOpenTelemetryMode.OFF); options.setDateProvider(new SentryAndroidDateProvider()); + options.setRuntimeManager(new AndroidRuntimeManager()); // set a lower flush timeout on Android to avoid ANRs options.setFlushTimeoutMillis(DEFAULT_FLUSH_TIMEOUT_MS); options.setFrameMetricsCollector( - new SentryFrameMetricsCollector(context, logger, buildInfoProvider)); + new SentryFrameMetricsCollector(finalContext, logger, buildInfoProvider)); - ManifestMetadataReader.applyMetadata(context, options, buildInfoProvider); - options.setCacheDirPath(getCacheDir(context).getAbsolutePath()); + ManifestMetadataReader.applyMetadata(finalContext, options, buildInfoProvider); - readDefaultOptionValues(options, context, buildInfoProvider); + options.setCacheDirPath( + options + .getRuntimeManager() + .runWithRelaxedPolicy(() -> getCacheDir(finalContext).getAbsolutePath())); + + readDefaultOptionValues(options, finalContext, buildInfoProvider); AppState.getInstance().registerLifecycleObserver(options); } 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 3c162aab1ad..12ec55f8e1b 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 @@ -20,10 +20,12 @@ import io.sentry.SentryOptions; import io.sentry.TracesSampler; import io.sentry.TracesSamplingDecision; +import io.sentry.android.core.internal.util.AndroidRuntimeManager; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.android.core.performance.TimeSpan; import io.sentry.util.AutoClosableReentrantLock; +import io.sentry.util.runtime.IRuntimeManager; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -108,7 +110,9 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri return; } - final @NotNull File cacheDir = AndroidOptionsInitializer.getCacheDir(context); + final @NotNull IRuntimeManager runtimeManager = new AndroidRuntimeManager(); + final @NotNull File cacheDir = + runtimeManager.runWithRelaxedPolicy(() -> AndroidOptionsInitializer.getCacheDir(context)); final @NotNull File configFile = new File(cacheDir, APP_START_PROFILING_CONFIG_FILE_NAME); // No config exists: app start profiling is not enabled diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java index 2829abc50c9..af57727a8d2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java @@ -19,6 +19,7 @@ import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.OutputStream; import org.jetbrains.annotations.ApiStatus; @@ -157,18 +158,18 @@ public static boolean hasStartupCrashMarker(final @NotNull SentryOptions options final File lastAnrMarker = new File(cacheDirPath, LAST_ANR_REPORT); try { - if (lastAnrMarker.exists() && lastAnrMarker.canRead()) { - final String content = FileUtils.readText(lastAnrMarker); - // we wrapped into try-catch already - //noinspection ConstantConditions - return content.equals("null") ? null : Long.parseLong(content.trim()); - } else { + final String content = FileUtils.readText(lastAnrMarker); + // we wrapped into try-catch already + //noinspection ConstantConditions + return content.equals("null") ? null : Long.parseLong(content.trim()); + } catch (Throwable e) { + if (e instanceof FileNotFoundException) { options .getLogger() .log(DEBUG, "Last ANR marker does not exist. %s.", lastAnrMarker.getAbsolutePath()); + } else { + options.getLogger().log(ERROR, "Error reading last ANR marker", e); } - } catch (Throwable e) { - options.getLogger().log(ERROR, "Error reading last ANR marker", e); } return null; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidRuntimeManager.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidRuntimeManager.java new file mode 100644 index 00000000000..b24ac70b5b0 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidRuntimeManager.java @@ -0,0 +1,23 @@ +package io.sentry.android.core.internal.util; + +import android.os.StrictMode; +import io.sentry.util.runtime.IRuntimeManager; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class AndroidRuntimeManager implements IRuntimeManager { + @Override + public T runWithRelaxedPolicy(final @NotNull IRuntimeManagerCallback toRun) { + final @NotNull StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + final @NotNull StrictMode.VmPolicy oldVmPolicy = StrictMode.getVmPolicy(); + StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.LAX); + StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX); + try { + return toRun.run(); + } finally { + StrictMode.setThreadPolicy(oldPolicy); + StrictMode.setVmPolicy(oldVmPolicy); + } + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CpuInfoUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CpuInfoUtils.java index 019db99fc7d..c289a9c06ac 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CpuInfoUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CpuInfoUtils.java @@ -51,8 +51,6 @@ private CpuInfoUtils() {} if (!cpuDir.getName().matches("cpu[0-9]+")) continue; File cpuMaxFreqFile = new File(cpuDir, CPUINFO_MAX_FREQ_PATH); - if (!cpuMaxFreqFile.exists() || !cpuMaxFreqFile.canRead()) continue; - long khz; try { String content = FileUtils.readText(cpuMaxFreqFile); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index d49a905772d..dc6926c1b17 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -24,6 +24,7 @@ import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator import io.sentry.android.core.internal.modules.AssetsModulesLoader import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider +import io.sentry.android.core.internal.util.AndroidRuntimeManager import io.sentry.android.core.internal.util.AndroidThreadChecker import io.sentry.android.core.performance.AppStartMetrics import io.sentry.android.fragment.FragmentLifecycleIntegration @@ -917,4 +918,10 @@ class AndroidOptionsInitializerTest { fixture.sentryOptions.compositePerformanceCollector is DefaultCompositePerformanceCollector } } + + @Test + fun `AndroidRuntimeManager is set in the options`() { + fixture.initSut() + assertIs(fixture.sentryOptions.runtimeManager) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidRuntimeManagerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidRuntimeManagerTest.kt new file mode 100644 index 00000000000..427d1287423 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidRuntimeManagerTest.kt @@ -0,0 +1,87 @@ +package io.sentry.android.core.internal.util + +import android.os.StrictMode +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AndroidRuntimeManagerTest { + + val sut = AndroidRuntimeManager() + + @AfterTest + fun `clean up`() { + // Revert StrictMode policies to avoid issues with other tests + StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.LAX) + StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX) + } + + @Test + fun `runWithRelaxedPolicy changes policy when running and restores it afterwards`() { + var called = false + val threadPolicy = StrictMode.ThreadPolicy.Builder().detectAll().penaltyDeath().build() + val vmPolicy = StrictMode.VmPolicy.Builder().detectAll().penaltyDeath().build() + assertNotEquals(StrictMode.ThreadPolicy.LAX, threadPolicy) + assertNotEquals(StrictMode.VmPolicy.LAX, vmPolicy) + + // Set and assert the StrictMode policies + StrictMode.setThreadPolicy(threadPolicy) + StrictMode.setVmPolicy(vmPolicy) + assertEquals(threadPolicy.toString(), StrictMode.getThreadPolicy().toString()) + assertEquals(vmPolicy.toString(), StrictMode.getVmPolicy().toString()) + + // Run the function and assert LAX policies + called = + sut.runWithRelaxedPolicy { + assertEquals( + StrictMode.ThreadPolicy.LAX.toString(), + StrictMode.getThreadPolicy().toString(), + ) + assertEquals(StrictMode.VmPolicy.LAX.toString(), StrictMode.getVmPolicy().toString()) + true + } + + // Policies should be reverted back + assertEquals(threadPolicy.toString(), StrictMode.getThreadPolicy().toString()) + assertEquals(vmPolicy.toString(), StrictMode.getVmPolicy().toString()) + + // Ensure the code ran + assertTrue(called) + } + + @Test + fun `runWithRelaxedPolicy changes policy and restores it afterwards even if the code throws`() { + var called = false + val threadPolicy = StrictMode.ThreadPolicy.Builder().detectAll().penaltyDeath().build() + val vmPolicy = StrictMode.VmPolicy.Builder().detectAll().penaltyDeath().build() + + // Set and assert the StrictMode policies + StrictMode.setThreadPolicy(threadPolicy) + StrictMode.setVmPolicy(vmPolicy) + + // Run the function and assert LAX policies + try { + sut.runWithRelaxedPolicy { + assertEquals( + StrictMode.ThreadPolicy.LAX.toString(), + StrictMode.getThreadPolicy().toString(), + ) + assertEquals(StrictMode.VmPolicy.LAX.toString(), StrictMode.getVmPolicy().toString()) + called = true + throw Exception("Test exception") + } + } catch (_: Exception) {} + + // Policies should be reverted back + assertEquals(threadPolicy.toString(), StrictMode.getThreadPolicy().toString()) + assertEquals(vmPolicy.toString(), StrictMode.getVmPolicy().toString()) + + // Ensure the code ran + assertTrue(called) + } +} 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 d3a60d2c198..8a6dc50c1fa 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 @@ -1,5 +1,6 @@ package io.sentry.uitest.android +import android.os.StrictMode import androidx.lifecycle.Lifecycle import androidx.test.core.app.launchActivity import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -252,6 +253,15 @@ class SdkInitTests : BaseUiTest() { assertDefaultIntegrations() } + @Test + fun initNotThrowStrictMode() { + StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().detectAll().penaltyDeath().build()) + StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyDeath().build()) + val sampleScenario = launchActivity() + initSentry() + sampleScenario.moveToState(Lifecycle.State.DESTROYED) + } + private fun assertDefaultIntegrations() { val integrations = mutableListOf( diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 23b75e9aa05..3964619e0b9 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3394,6 +3394,7 @@ public class io/sentry/SentryOptions { public fun getReadTimeoutMillis ()I public fun getRelease ()Ljava/lang/String; public fun getReplayController ()Lio/sentry/ReplayController; + public fun getRuntimeManager ()Lio/sentry/util/runtime/IRuntimeManager; public fun getSampleRate ()Ljava/lang/Double; public fun getScopeObservers ()Ljava/util/List; public fun getSdkVersion ()Lio/sentry/protocol/SdkVersion; @@ -3539,6 +3540,7 @@ public class io/sentry/SentryOptions { public fun setReadTimeoutMillis (I)V public fun setRelease (Ljava/lang/String;)V public fun setReplayController (Lio/sentry/ReplayController;)V + public fun setRuntimeManager (Lio/sentry/util/runtime/IRuntimeManager;)V public fun setSampleRate (Ljava/lang/Double;)V public fun setSdkVersion (Lio/sentry/protocol/SdkVersion;)V public fun setSendClientReports (Z)V @@ -7293,6 +7295,19 @@ public final class io/sentry/util/UrlUtils$UrlDetails { public fun getUrlOrFallback ()Ljava/lang/String; } +public abstract interface class io/sentry/util/runtime/IRuntimeManager { + public abstract fun runWithRelaxedPolicy (Lio/sentry/util/runtime/IRuntimeManager$IRuntimeManagerCallback;)Ljava/lang/Object; +} + +public abstract interface class io/sentry/util/runtime/IRuntimeManager$IRuntimeManagerCallback { + public abstract fun run ()Ljava/lang/Object; +} + +public final class io/sentry/util/runtime/NeutralRuntimeManager : io/sentry/util/runtime/IRuntimeManager { + public fun ()V + public fun runWithRelaxedPolicy (Lio/sentry/util/runtime/IRuntimeManager$IRuntimeManagerCallback;)Ljava/lang/Object; +} + public abstract interface class io/sentry/util/thread/IThreadChecker { public abstract fun currentThreadSystemId ()J public abstract fun getCurrentThreadName ()Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/DirectoryProcessor.java b/sentry/src/main/java/io/sentry/DirectoryProcessor.java index a6bb258f30d..1bc277b18cd 100644 --- a/sentry/src/main/java/io/sentry/DirectoryProcessor.java +++ b/sentry/src/main/java/io/sentry/DirectoryProcessor.java @@ -40,34 +40,22 @@ public void processDirectory(final @NotNull File directory) { try { logger.log(SentryLevel.DEBUG, "Processing dir. %s", directory.getAbsolutePath()); - if (!directory.exists()) { + final File[] filteredListFiles = directory.listFiles((d, name) -> isRelevantFileName(name)); + if (filteredListFiles == null) { logger.log( - SentryLevel.WARNING, - "Directory '%s' doesn't exist. No cached events to send.", + SentryLevel.ERROR, + "Cache dir %s is null or is not a directory.", directory.getAbsolutePath()); return; } - if (!directory.isDirectory()) { - logger.log( - SentryLevel.ERROR, "Cache dir %s is not a directory.", directory.getAbsolutePath()); - return; - } - - final File[] listFiles = directory.listFiles(); - if (listFiles == null) { - logger.log(SentryLevel.ERROR, "Cache dir %s is null.", directory.getAbsolutePath()); - return; - } - - final File[] filteredListFiles = directory.listFiles((d, name) -> isRelevantFileName(name)); logger.log( SentryLevel.DEBUG, "Processing %d items from cache dir %s", - filteredListFiles != null ? filteredListFiles.length : 0, + filteredListFiles.length, directory.getAbsolutePath()); - for (File file : listFiles) { + for (File file : filteredListFiles) { // it ignores .sentry-native database folder and new ones that might come up if (!file.isFile()) { logger.log(SentryLevel.DEBUG, "File %s is not a File.", file.getAbsolutePath()); diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 4aee71715d7..a28ee936e62 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -580,7 +580,8 @@ private static boolean preInitConfigurations(final @NotNull SentryOptions option return true; } - @SuppressWarnings("FutureReturnValueIgnored") + // older AGP versions do not support method references + @SuppressWarnings({"FutureReturnValueIgnored", "Convert2MethodRef"}) private static void initConfigurations(final @NotNull SentryOptions options) { final @NotNull ILogger logger = options.getLogger(); logger.log(SentryLevel.INFO, "Initializing SDK with DSN: '%s'", options.getDsn()); @@ -592,7 +593,7 @@ private static void initConfigurations(final @NotNull SentryOptions options) { final String outboxPath = options.getOutboxPath(); if (outboxPath != null) { final File outboxDir = new File(outboxPath); - outboxDir.mkdirs(); + options.getRuntimeManager().runWithRelaxedPolicy(() -> outboxDir.mkdirs()); } else { logger.log(SentryLevel.INFO, "No outbox dir path is defined in options."); } @@ -600,7 +601,7 @@ private static void initConfigurations(final @NotNull SentryOptions options) { final String cacheDirPath = options.getCacheDirPath(); if (cacheDirPath != null) { final File cacheDir = new File(cacheDirPath); - cacheDir.mkdirs(); + options.getRuntimeManager().runWithRelaxedPolicy(() -> cacheDir.mkdirs()); final IEnvelopeCache envelopeCache = options.getEnvelopeDiskCache(); // only overwrite the cache impl if it's not already set if (envelopeCache instanceof NoOpEnvelopeCache) { @@ -613,7 +614,7 @@ private static void initConfigurations(final @NotNull SentryOptions options) { && profilingTracesDirPath != null) { final File profilingTracesDir = new File(profilingTracesDirPath); - profilingTracesDir.mkdirs(); + options.getRuntimeManager().runWithRelaxedPolicy(() -> profilingTracesDir.mkdirs()); try { options diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index f868ecacad2..3a81b268b25 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -27,6 +27,8 @@ import io.sentry.util.Platform; import io.sentry.util.SampleRateUtils; import io.sentry.util.StringUtils; +import io.sentry.util.runtime.IRuntimeManager; +import io.sentry.util.runtime.NeutralRuntimeManager; import io.sentry.util.thread.IThreadChecker; import io.sentry.util.thread.NoOpThreadChecker; import java.io.File; @@ -597,6 +599,9 @@ public class SentryOptions { private @NotNull ISocketTagger socketTagger = NoOpSocketTagger.getInstance(); + /** Runtime manager to manage runtime policies, like StrictMode on Android. */ + private @NotNull IRuntimeManager runtimeManager = new NeutralRuntimeManager(); + private @Nullable String profilingTracesDirPath; /** @@ -3004,6 +3009,26 @@ public void setSocketTagger(final @Nullable ISocketTagger socketTagger) { this.socketTagger = socketTagger != null ? socketTagger : NoOpSocketTagger.getInstance(); } + /** + * Returns the IRuntimeManager + * + * @return the runtime manager + */ + @ApiStatus.Internal + public @NotNull IRuntimeManager getRuntimeManager() { + return runtimeManager; + } + + /** + * Sets the IRuntimeManager + * + * @param runtimeManager the runtime manager + */ + @ApiStatus.Internal + public void setRuntimeManager(final @NotNull IRuntimeManager runtimeManager) { + this.runtimeManager = runtimeManager; + } + /** * Load the lazy fields. Useful to load in the background, so that results are already cached. DO * NOT CALL THIS METHOD ON THE MAIN THREAD. diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index d2a32d44cd0..893bef4ab90 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -336,18 +336,17 @@ public void discard(final @NotNull SentryEnvelope envelope) { Objects.requireNonNull(envelope, "Envelope is required."); final File envelopeFile = getEnvelopeFile(envelope); - if (envelopeFile.exists()) { + if (envelopeFile.delete()) { options .getLogger() .log(DEBUG, "Discarding envelope from cache: %s", envelopeFile.getAbsolutePath()); - - if (!envelopeFile.delete()) { - options - .getLogger() - .log(ERROR, "Failed to delete envelope: %s", envelopeFile.getAbsolutePath()); - } } else { - options.getLogger().log(DEBUG, "Envelope was not cached: %s", envelopeFile.getAbsolutePath()); + options + .getLogger() + .log( + DEBUG, + "Envelope was not cached or could not be deleted: %s", + envelopeFile.getAbsolutePath()); } } diff --git a/sentry/src/main/java/io/sentry/util/runtime/IRuntimeManager.java b/sentry/src/main/java/io/sentry/util/runtime/IRuntimeManager.java new file mode 100644 index 00000000000..f6e27cce89b --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/runtime/IRuntimeManager.java @@ -0,0 +1,13 @@ +package io.sentry.util.runtime; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public interface IRuntimeManager { + T runWithRelaxedPolicy(final @NotNull IRuntimeManagerCallback toRun); + + interface IRuntimeManagerCallback { + T run(); + } +} diff --git a/sentry/src/main/java/io/sentry/util/runtime/NeutralRuntimeManager.java b/sentry/src/main/java/io/sentry/util/runtime/NeutralRuntimeManager.java new file mode 100644 index 00000000000..a6112ff60b3 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/runtime/NeutralRuntimeManager.java @@ -0,0 +1,12 @@ +package io.sentry.util.runtime; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class NeutralRuntimeManager implements IRuntimeManager { + @Override + public T runWithRelaxedPolicy(final @NotNull IRuntimeManagerCallback toRun) { + return toRun.run(); + } +} diff --git a/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt b/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt index 4ddc5561af8..dce7c3d5863 100644 --- a/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt +++ b/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt @@ -61,8 +61,8 @@ class EnvelopeSenderTest { val sut = fixture.getSut() sut.processDirectory(File("i don't exist")) verify(fixture.logger)!!.log( - eq(SentryLevel.WARNING), - eq("Directory '%s' doesn't exist. No cached events to send."), + eq(SentryLevel.ERROR), + eq("Cache dir %s is null or is not a directory."), any(), ) verifyNoMoreInteractions(fixture.scopes) @@ -79,14 +79,14 @@ class EnvelopeSenderTest { sut.processDirectory(testFile) verify(fixture.logger)!!.log( eq(SentryLevel.ERROR), - eq("Cache dir %s is not a directory."), + eq("Cache dir %s is null or is not a directory."), any(), ) verifyNoMoreInteractions(fixture.scopes) } @Test - fun `when directory has non event files, processDirectory logs that`() { + fun `when directory has non event files, processDirectory skips them`() { val sut = fixture.getSut() val testFile = File( @@ -96,7 +96,8 @@ class EnvelopeSenderTest { testFile.deleteOnExit() verify(fixture.logger)!!.log( eq(SentryLevel.DEBUG), - eq("File '%s' doesn't match extension expected."), + eq("Processing %d items from cache dir %s"), + eq(0), any(), ) verify(fixture.scopes, never())!!.captureEnvelope(any(), anyOrNull()) diff --git a/sentry/src/test/java/io/sentry/util/runtime/NeutralRuntimeManagerTest.kt b/sentry/src/test/java/io/sentry/util/runtime/NeutralRuntimeManagerTest.kt new file mode 100644 index 00000000000..a645915d7ab --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/runtime/NeutralRuntimeManagerTest.kt @@ -0,0 +1,19 @@ +package io.sentry.util.runtime + +import kotlin.test.Test +import kotlin.test.assertTrue + +class NeutralRuntimeManagerTest { + + val sut = NeutralRuntimeManager() + + @Test + fun `runWithRelaxedPolicy runs the code`() { + var called = false + + called = sut.runWithRelaxedPolicy { true } + + // Ensure the code ran + assertTrue(called) + } +} From b23af9ee93b9c087536321d44d18dc8f521c0b78 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 16 Oct 2025 11:28:59 +0200 Subject: [PATCH 004/546] chore(samples): Add Custom Tabs screen (#4799) --- gradle/libs.versions.toml | 1 + .../sentry-samples-android/build.gradle.kts | 1 + .../src/main/AndroidManifest.xml | 8 +++-- .../samples/android/CustomTabsActivity.java | 35 +++++++++++++++++++ .../sentry/samples/android/MainActivity.java | 5 +++ .../src/main/res/layout/activity_main.xml | 7 ++++ .../src/main/res/values/strings.xml | 1 + 7 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/CustomTabsActivity.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 19415e87ca2..6672de2d6e7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -91,6 +91,7 @@ androidx-navigation-runtime = { module = "androidx.navigation:navigation-runtime androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigation" } androidx-sqlite = { module = "androidx.sqlite:sqlite", version = "2.5.2" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version = "1.2.1" } +androidx-browser = { module = "androidx.browser:browser", version = "1.8.0" } coil-compose = { module = "io.coil-kt:coil-compose", version = "2.6.0" } commons-compress = {module = "org.apache.commons:commons-compress", version = "1.25.0"} context-propagation = { module = "io.micrometer:context-propagation", version = "1.1.0" } diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index 48ac6dda5ce..c2ea72485ce 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -144,6 +144,7 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.recyclerview) + implementation(libs.androidx.browser) implementation(libs.coil.compose) implementation(libs.kotlinx.coroutines.android) implementation(libs.retrofit) diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index c6360ca911d..c4f90bcc7c7 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -69,8 +69,12 @@ android:exported="false" /> + android:name=".CustomTabsActivity" + android:exported="false" /> + + diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/CustomTabsActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/CustomTabsActivity.java new file mode 100644 index 00000000000..4f615137ede --- /dev/null +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/CustomTabsActivity.java @@ -0,0 +1,35 @@ +package io.sentry.samples.android; + +import android.net.Uri; +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.browser.customtabs.CustomTabColorSchemeParams; +import androidx.browser.customtabs.CustomTabsIntent; +import androidx.core.content.ContextCompat; + +public class CustomTabsActivity extends AppCompatActivity { + + private static final String DEMO_URL = "https://www.sentry.io/"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); + + CustomTabColorSchemeParams params = + new CustomTabColorSchemeParams.Builder() + .setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary)) + .build(); + builder.setDefaultColorSchemeParams(params); + + builder.setShowTitle(true); + builder.setShareState(CustomTabsIntent.SHARE_STATE_ON); + builder.setInstantAppsEnabled(true); + + CustomTabsIntent customTabsIntent = builder.build(); + customTabsIntent.launchUrl(this, Uri.parse(DEMO_URL)); + + finish(); + } +} diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index 62280ecd763..0987dafe3fa 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -272,6 +272,11 @@ public void run() { startActivity(new Intent(this, ProfilingActivity.class)); }); + binding.openCustomTabsActivity.setOnClickListener( + view -> { + startActivity(new Intent(this, CustomTabsActivity.class)); + }); + binding.openFrameDataForSpans.setOnClickListener( view -> startActivity(new Intent(this, FrameDataForSpansActivity.class))); diff --git a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml index bccb56ca830..674b65199ae 100644 --- a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml +++ b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml @@ -146,6 +146,13 @@ android:layout_height="wrap_content" android:text="@string/open_profiling_activity"/> +