Skip to content

Commit d8050f0

Browse files
committed
Check for compose classes via reflection instead of try-catch
1 parent d1a0f7b commit d8050f0

File tree

6 files changed

+78
-16
lines changed

6 files changed

+78
-16
lines changed

buildSrc/src/main/java/Config.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ object Config {
131131
val composeNavigation = "androidx.navigation:navigation-compose:$navigationVersion"
132132
val composeActivity = "androidx.activity:activity-compose:1.4.0"
133133
val composeFoundation = "androidx.compose.foundation:foundation:$composeVersion"
134+
val composeUi = "androidx.compose.ui:ui:$composeVersion"
134135
val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:$composeVersion"
135136
val composeMaterial = "androidx.compose.material3:material3:1.0.0-alpha13"
136137

sentry-android-core/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ dependencies {
108108
testImplementation(projects.sentryTestSupport)
109109
testImplementation(projects.sentryAndroidFragment)
110110
testImplementation(projects.sentryAndroidTimber)
111+
testImplementation(projects.sentryComposeHelper)
112+
testImplementation(projects.sentryAndroidNdk)
111113
testRuntimeOnly(Config.Libs.timber)
112114
testRuntimeOnly(Config.Libs.fragment)
113115
}

sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
@SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references
4242
final class AndroidOptionsInitializer {
4343

44+
static final String SENTRY_COMPOSE_INTEGRATION_CLASS_NAME =
45+
"io.sentry.compose.gestures.ComposeGestureTargetLocator";
46+
47+
static final String COMPOSE_CLASS_NAME = "androidx.compose.ui.node.Owner";
48+
4449
/** private ctor */
4550
private AndroidOptionsInitializer() {}
4651

@@ -143,15 +148,15 @@ static void initializeIntegrationsAndProcessors(
143148
if (options.getGestureTargetLocators().isEmpty()) {
144149
final List<GestureTargetLocator> gestureTargetLocators = new ArrayList<>(2);
145150
gestureTargetLocators.add(new AndroidViewGestureTargetLocator(isAndroidXScrollViewAvailable));
146-
try {
151+
152+
final boolean isComposeUpstreamAvailable =
153+
loadClass.isClassAvailable(COMPOSE_CLASS_NAME, options);
154+
final boolean isComposeAvailable =
155+
(isComposeUpstreamAvailable
156+
&& loadClass.isClassAvailable(SENTRY_COMPOSE_INTEGRATION_CLASS_NAME, options));
157+
158+
if (isComposeAvailable) {
147159
gestureTargetLocators.add(new ComposeGestureTargetLocator());
148-
} catch (NoClassDefFoundError error) {
149-
options
150-
.getLogger()
151-
.log(
152-
SentryLevel.DEBUG,
153-
"ComposeGestureTargetLocator not available, consider adding the `sentry-compose` library.",
154-
error);
155160
}
156161
options.setGestureTargetLocators(gestureTargetLocators);
157162
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<uses-sdk
5+
tools:overrideLibrary="
6+
io.sentry.compose,
7+
io.sentry.android.ndk,
8+
androidx.navigation.compose,
9+
androidx.activity.compose,
10+
androidx.compose.animation,
11+
androidx.lifecycle.viewmodel.compose,
12+
androidx.compose.foundation.layout,
13+
androidx.compose.animation.core,
14+
androidx.compose.ui,
15+
androidx.compose.ui.text,
16+
androidx.compose.runtime.saveable,
17+
androidx.compose.ui.graphics,
18+
androidx.compose.ui.unit,
19+
androidx.compose.ui.geometry,
20+
androidx.compose.ui.util"/>
21+
</manifest>

sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import io.sentry.ILogger
88
import io.sentry.MainEventProcessor
99
import io.sentry.SentryOptions
1010
import io.sentry.android.core.cache.AndroidEnvelopeCache
11+
import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator
1112
import io.sentry.android.core.internal.modules.AssetsModulesLoader
1213
import io.sentry.android.core.internal.util.AndroidMainThreadChecker
1314
import io.sentry.android.fragment.FragmentLifecycleIntegration
1415
import io.sentry.android.timber.SentryTimberIntegration
16+
import io.sentry.compose.gestures.ComposeGestureTargetLocator
1517
import org.junit.runner.RunWith
1618
import org.mockito.kotlin.any
19+
import org.mockito.kotlin.eq
1720
import org.mockito.kotlin.mock
1821
import org.mockito.kotlin.whenever
1922
import java.io.File
@@ -66,7 +69,7 @@ class AndroidOptionsInitializerTest {
6669

6770
fun initSutWithClassLoader(
6871
minApi: Int = 16,
69-
classToLoad: Class<*>? = null,
72+
classesToLoad: List<String> = emptyList(),
7073
isFragmentAvailable: Boolean = false,
7174
isTimberAvailable: Boolean = false
7275
) {
@@ -89,7 +92,7 @@ class AndroidOptionsInitializerTest {
8992
sentryOptions,
9093
context,
9194
buildInfo,
92-
createClassMock(classToLoad),
95+
createClassMock(classesToLoad),
9396
isFragmentAvailable,
9497
isTimberAvailable
9598
)
@@ -101,10 +104,14 @@ class AndroidOptionsInitializerTest {
101104
return buildInfo
102105
}
103106

104-
private fun createClassMock(clazz: Class<*>?): LoadClass {
107+
private fun createClassMock(classes: List<String>): LoadClass {
105108
val loadClassMock = mock<LoadClass>()
106-
whenever(loadClassMock.loadClass(any(), any())).thenReturn(clazz)
107-
whenever(loadClassMock.isClassAvailable(any(), any<ILogger>())).thenReturn(clazz != null)
109+
classes.forEach {
110+
whenever(loadClassMock.loadClass(eq(it), any()))
111+
.thenReturn(Class.forName(it, false, this::class.java.classLoader))
112+
whenever(loadClassMock.isClassAvailable(eq(it), any<SentryOptions>()))
113+
.thenReturn(true)
114+
}
108115
return loadClassMock
109116
}
110117
}
@@ -267,23 +274,23 @@ class AndroidOptionsInitializerTest {
267274

268275
@Test
269276
fun `NdkIntegration will load SentryNdk class and add to the integration list`() {
270-
fixture.initSutWithClassLoader(classToLoad = SentryNdk::class.java)
277+
fixture.initSutWithClassLoader(classesToLoad = listOfNotNull(NdkIntegration.SENTRY_NDK_CLASS_NAME))
271278

272279
val actual = fixture.sentryOptions.integrations.firstOrNull { it is NdkIntegration }
273280
assertNotNull((actual as NdkIntegration).sentryNdkClass)
274281
}
275282

276283
@Test
277284
fun `NdkIntegration won't be enabled because API is lower than 16`() {
278-
fixture.initSutWithClassLoader(minApi = 14, classToLoad = SentryNdk::class.java)
285+
fixture.initSutWithClassLoader(minApi = 14, classesToLoad = listOfNotNull(NdkIntegration.SENTRY_NDK_CLASS_NAME))
279286

280287
val actual = fixture.sentryOptions.integrations.firstOrNull { it is NdkIntegration }
281288
assertNull((actual as NdkIntegration).sentryNdkClass)
282289
}
283290

284291
@Test
285292
fun `NdkIntegration won't be enabled, if class not found`() {
286-
fixture.initSutWithClassLoader(classToLoad = null)
293+
fixture.initSutWithClassLoader(classesToLoad = emptyList())
287294

288295
val actual = fixture.sentryOptions.integrations.firstOrNull { it is NdkIntegration }
289296
assertNull((actual as NdkIntegration).sentryNdkClass)
@@ -455,4 +462,26 @@ class AndroidOptionsInitializerTest {
455462

456463
assertTrue { fixture.sentryOptions.mainThreadChecker is AndroidMainThreadChecker }
457464
}
465+
466+
@Test
467+
fun `does not install ComposeGestureTargetLocator, if sentry-compose is not available`() {
468+
fixture.initSutWithClassLoader()
469+
470+
assertTrue { fixture.sentryOptions.gestureTargetLocators.size == 1 }
471+
assertTrue { fixture.sentryOptions.gestureTargetLocators[0] is AndroidViewGestureTargetLocator }
472+
}
473+
474+
@Test
475+
fun `installs ComposeGestureTargetLocator, if sentry-compose is available`() {
476+
fixture.initSutWithClassLoader(
477+
classesToLoad = listOf(
478+
AndroidOptionsInitializer.COMPOSE_CLASS_NAME,
479+
AndroidOptionsInitializer.SENTRY_COMPOSE_INTEGRATION_CLASS_NAME
480+
)
481+
)
482+
483+
assertTrue { fixture.sentryOptions.gestureTargetLocators.size == 2 }
484+
assertTrue { fixture.sentryOptions.gestureTargetLocators[0] is AndroidViewGestureTargetLocator }
485+
assertTrue { fixture.sentryOptions.gestureTargetLocators[1] is ComposeGestureTargetLocator }
486+
}
458487
}

sentry-compose/proguard-rules.pro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
##---------------Begin: proguard configuration for Compose ----------
22

3+
# The Android SDK checks at runtime if these classes are available via Class.forName
4+
-keep class io.sentry.compose.gestures.ComposeGestureTargetLocator { <init>(...); }
5+
-keepnames interface androidx.compose.ui.node.Owner
6+
37
# To ensure that stack traces is unambiguous
48
# https://developer.android.com/studio/build/shrink-code#decode-stack-trace
59
-keepattributes LineNumberTable,SourceFile

0 commit comments

Comments
 (0)