diff --git a/CHANGELOG.md b/CHANGELOG.md index 301ded35e2..cd25dd724f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* Fix: Check at runtime if AndroidX.Core is available (#1718) * Feat: Add "data" to spans (#1717) * Fix: Should not capture unfinished transaction (#1719) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java index 8858bffdac..7e0b6195ff 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java @@ -14,25 +14,52 @@ final class ActivityFramesTracker { - private final @NotNull FrameMetricsAggregator frameMetricsAggregator; + private @Nullable FrameMetricsAggregator frameMetricsAggregator = null; + private boolean androidXAvailable = true; - ActivityFramesTracker() { - this(new FrameMetricsAggregator()); + private final @NotNull Map> + activityMeasurements = new ConcurrentHashMap<>(); + + ActivityFramesTracker(final @NotNull LoadClass loadClass) { + androidXAvailable = checkAndroidXAvailability(loadClass); + if (androidXAvailable) { + frameMetricsAggregator = new FrameMetricsAggregator(); + } } @TestOnly - ActivityFramesTracker(final @NotNull FrameMetricsAggregator frameMetricsAggregator) { + ActivityFramesTracker(final @Nullable FrameMetricsAggregator frameMetricsAggregator) { this.frameMetricsAggregator = frameMetricsAggregator; } - private final @NotNull Map> - activityMeasurements = new ConcurrentHashMap<>(); + private static boolean checkAndroidXAvailability(final @NotNull LoadClass loadClass) { + try { + loadClass.loadClass("androidx.core.app.FrameMetricsAggregator"); + return true; + } catch (ClassNotFoundException ignored) { + // androidx.core isn't available. + return false; + } + } + + private boolean isFrameMetricsAggregatorAvailable() { + return androidXAvailable && frameMetricsAggregator != null; + } + @SuppressWarnings("NullAway") void addActivity(final @NotNull Activity activity) { + if (!isFrameMetricsAggregatorAvailable()) { + return; + } frameMetricsAggregator.add(activity); } + @SuppressWarnings("NullAway") void setMetrics(final @NotNull Activity activity, final @NotNull SentryId sentryId) { + if (!isFrameMetricsAggregatorAvailable()) { + return; + } + int totalFrames = 0; int slowFrames = 0; int frozenFrames = 0; @@ -75,14 +102,21 @@ void setMetrics(final @NotNull Activity activity, final @NotNull SentryId sentry @Nullable Map takeMetrics(final @NotNull SentryId sentryId) { + if (!isFrameMetricsAggregatorAvailable()) { + return null; + } + final Map stringMeasurementValueMap = activityMeasurements.get(sentryId); activityMeasurements.remove(sentryId); return stringMeasurementValueMap; } + @SuppressWarnings("NullAway") void stop() { - frameMetricsAggregator.stop(); + if (isFrameMetricsAggregatorAvailable()) { + frameMetricsAggregator.stop(); + } activityMeasurements.clear(); } } 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 2d6d7a7be8..2779e26d8c 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 @@ -76,14 +76,14 @@ static void init( * @param context the Application context * @param logger the ILogger interface * @param buildInfoProvider the IBuildInfoProvider interface - * @param loadClass the ILoadClass interface + * @param loadClass the LoadClass wrapper */ static void init( final @NotNull SentryAndroidOptions options, @NotNull Context context, final @NotNull ILogger logger, final @NotNull IBuildInfoProvider buildInfoProvider, - final @NotNull ILoadClass loadClass) { + final @NotNull LoadClass loadClass) { Objects.requireNonNull(context, "The context is required."); // it returns null if ContextImpl, so let's check for nullability @@ -100,7 +100,7 @@ static void init( ManifestMetadataReader.applyMetadata(context, options); initializeCacheDirs(context, options); - final ActivityFramesTracker activityFramesTracker = new ActivityFramesTracker(); + final ActivityFramesTracker activityFramesTracker = new ActivityFramesTracker(loadClass); installDefaultIntegrations( context, options, buildInfoProvider, loadClass, activityFramesTracker); @@ -116,7 +116,7 @@ private static void installDefaultIntegrations( final @NotNull Context context, final @NotNull SentryOptions options, final @NotNull IBuildInfoProvider buildInfoProvider, - final @NotNull ILoadClass loadClass, + final @NotNull LoadClass loadClass, final @NotNull ActivityFramesTracker activityFramesTracker) { options.addIntegration( @@ -224,7 +224,7 @@ private static boolean isNdkAvailable(final @NotNull IBuildInfoProvider buildInf private static @Nullable Class loadNdkIfAvailable( final @NotNull SentryOptions options, final @NotNull IBuildInfoProvider buildInfoProvider, - final @NotNull ILoadClass loadClass) { + final @NotNull LoadClass loadClass) { if (isNdkAvailable(buildInfoProvider)) { try { return loadClass.loadClass(SENTRY_NDK_CLASS_NAME); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ILoadClass.java b/sentry-android-core/src/main/java/io/sentry/android/core/ILoadClass.java deleted file mode 100644 index c6c647a171..0000000000 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ILoadClass.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.sentry.android.core; - -import org.jetbrains.annotations.NotNull; - -/** An Adapter for making Class.forName testable */ -interface ILoadClass { - - /** - * Try to load a class via reflection - * - * @param clazz the full class name - * @return a Class - * @throws ClassNotFoundException if class is not found - */ - @NotNull - Class loadClass(String clazz) throws ClassNotFoundException; -} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java index e9afe562de..28ce508b92 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java @@ -2,8 +2,16 @@ import org.jetbrains.annotations.NotNull; -final class LoadClass implements ILoadClass { - @Override +/** An Adapter for making Class.forName testable */ +final class LoadClass { + + /** + * Try to load a class via reflection + * + * @param clazz the full class name + * @return a Class + * @throws ClassNotFoundException if class is not found + */ public @NotNull Class loadClass(@NotNull String clazz) throws ClassNotFoundException { return Class.forName(clazz); } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt index cf41f10b7c..71ae35318d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt @@ -20,6 +20,7 @@ class ActivityFramesTrackerTest { val aggregator = mock() val activity = mock() val sentryId = SentryId() + val loadClass = mock() fun getSut(): ActivityFramesTracker { return ActivityFramesTracker(aggregator) @@ -113,6 +114,38 @@ class ActivityFramesTrackerTest { assertNull(metrics) } + @Test + fun `addActivity does not throw if no AndroidX`() { + whenever(fixture.loadClass.loadClass(any())).thenThrow(ClassNotFoundException()) + val sut = ActivityFramesTracker(fixture.loadClass) + + sut.addActivity(fixture.activity) + } + + @Test + fun `setMetrics does not throw if no AndroidX`() { + whenever(fixture.loadClass.loadClass(any())).thenThrow(ClassNotFoundException()) + val sut = ActivityFramesTracker(fixture.loadClass) + + sut.setMetrics(fixture.activity, fixture.sentryId) + } + + @Test + fun `stop does not throw if no AndroidX`() { + whenever(fixture.loadClass.loadClass(any())).thenThrow(ClassNotFoundException()) + val sut = ActivityFramesTracker(fixture.loadClass) + + sut.stop() + } + + @Test + fun `takeMetrics returns null if no AndroidX`() { + whenever(fixture.loadClass.loadClass(any())).thenThrow(ClassNotFoundException()) + val sut = ActivityFramesTracker(fixture.loadClass) + + assertNull(sut.takeMetrics(fixture.sentryId)) + } + private fun getArray(frameTime: Int = 1, numFrames: Int = 1): Array { val totalArray = SparseIntArray() totalArray.put(frameTime, numFrames) 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 9622c2d602..3d571811ea 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 @@ -16,6 +16,7 @@ import io.sentry.MainEventProcessor import io.sentry.SendCachedEnvelopeFireAndForgetIntegration import io.sentry.SentryLevel import io.sentry.SentryOptions +import io.sentry.android.core.NdkIntegration.SENTRY_NDK_CLASS_NAME import java.io.File import java.lang.RuntimeException import kotlin.test.BeforeTest @@ -332,15 +333,15 @@ class AndroidOptionsInitializerTest { return buildInfo } - private fun createClassMock(clazz: Class<*> = SentryNdk::class.java): ILoadClass { - val loadClassMock = mock() + private fun createClassMock(clazz: Class<*> = SentryNdk::class.java): LoadClass { + val loadClassMock = mock() whenever(loadClassMock.loadClass(any())).thenReturn(clazz) return loadClassMock } - private fun createClassMockThrows(ex: Throwable): ILoadClass { - val loadClassMock = mock() - whenever(loadClassMock.loadClass(any())).thenThrow(ex) + private fun createClassMockThrows(ex: Throwable): LoadClass { + val loadClassMock = mock() + whenever(loadClassMock.loadClass(eq(SENTRY_NDK_CLASS_NAME))).thenThrow(ex) return loadClassMock } } diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index de2e016d8b..c09a14f9df 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -105,6 +105,7 @@ dependencies { // implementation(projects.sentryAndroid) { // exclude(group = "androidx.lifecycle", module = "lifecycle-process") // exclude(group = "androidx.lifecycle", module = "lifecycle-common-java8") +// exclude(group = "androidx.core", module = "core") // } implementation(Config.Libs.appCompat)