diff --git a/.github/workflows/agp-matrix.yml b/.github/workflows/agp-matrix.yml new file mode 100644 index 0000000000..f2c918422b --- /dev/null +++ b/.github/workflows/agp-matrix.yml @@ -0,0 +1,44 @@ +name: AGP Matrix Sample Release + +on: + push: + branches: + - main + - release/** + pull_request: + +jobs: + cancel-previous-workflow: + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@b173b6ec0100793626c2d9e6b90435061f4fc3e5 # pin@0.11.0 + with: + access_token: ${{ github.token }} + + agp-matrix-sample-release: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + agp: ['7.3.0','7.4.0-rc01','8.0.0-alpha09'] + + name: AGP Matrix Sample Release - AGP ${{ matrix.agp }} + env: + VERSION_AGP: ${{ matrix.agp }} + + steps: + - name: Checkout Repo + uses: actions/checkout@v2 + + - name: Setup Java Version + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' + + - name: Build the Release variant + uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # pin@v2 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + arguments: sentry-android-integration-tests:sentry-test-agp:assembleRelease diff --git a/CHANGELOG.md b/CHANGELOG.md index 950ea4671e..7c6989b370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes - Use minSdk compatible `Objects` class ([#2436](https://github.com/getsentry/sentry-java/pull/2436)) +- Prevent R8 from warning on missing classes, as we check for their presence at runtime ([#2439](https://github.com/getsentry/sentry-java/pull/2439)) ### Features diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index a0a269115b..47d80f0d93 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -1,6 +1,7 @@ import java.math.BigDecimal object Config { + val AGP = System.getenv("VERSION_AGP") ?: "7.3.0" val kotlinVersion = "1.6.10" val kotlinStdLib = "stdlib-jdk8" @@ -11,7 +12,7 @@ object Config { val composeVersion = "1.1.1" object BuildPlugins { - val androidGradle = "com.android.tools.build:gradle:7.3.0" + val androidGradle = "com.android.tools.build:gradle:$AGP" val kotlinGradlePlugin = "gradle-plugin" val buildConfig = "com.github.gmazzo.buildconfig" val buildConfigVersion = "3.0.3" @@ -130,6 +131,7 @@ object Config { val composeNavigation = "androidx.navigation:navigation-compose:$navigationVersion" val composeActivity = "androidx.activity:activity-compose:1.4.0" val composeFoundation = "androidx.compose.foundation:foundation:$composeVersion" + val composeUi = "androidx.compose.ui:ui:$composeVersion" val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:$composeVersion" val composeMaterial = "androidx.compose.material3:material3:1.0.0-alpha13" diff --git a/gradle.properties b/gradle.properties index c2c4e22316..00a59c30bc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,9 @@ org.gradle.parallel=true # AndroidX required by AGP >= 3.6.x android.useAndroidX=true +# Required by AGP >= 8.0.x +android.defaults.buildfeatures.buildconfig=true + # Release information versionName=6.10.0 diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index 9b8a2623ac..de6ce20596 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -108,6 +108,8 @@ dependencies { testImplementation(projects.sentryTestSupport) testImplementation(projects.sentryAndroidFragment) testImplementation(projects.sentryAndroidTimber) + testImplementation(projects.sentryComposeHelper) + testImplementation(projects.sentryAndroidNdk) testRuntimeOnly(Config.Libs.timber) testRuntimeOnly(Config.Libs.fragment) } diff --git a/sentry-android-core/proguard-rules.pro b/sentry-android-core/proguard-rules.pro index 5b1934a8e3..b9d371c29f 100644 --- a/sentry-android-core/proguard-rules.pro +++ b/sentry-android-core/proguard-rules.pro @@ -20,10 +20,10 @@ # don't warn jetbrains annotations -dontwarn org.jetbrains.annotations.** -# don't warn about missing classes (mainly for Guardsquare's proguard). -# We are checking for their presence at runtime +# don't warn about missing classes, as we are checking for their presence at runtime -dontwarn io.sentry.android.timber.SentryTimberIntegration -dontwarn io.sentry.android.fragment.FragmentLifecycleIntegration +-dontwarn io.sentry.compose.gestures.ComposeGestureTargetLocator # To ensure that stack traces is unambiguous # https://developer.android.com/studio/build/shrink-code#decode-stack-trace 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 620fbed05c..5b3e473743 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 @@ -41,6 +41,11 @@ @SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references final class AndroidOptionsInitializer { + static final String SENTRY_COMPOSE_INTEGRATION_CLASS_NAME = + "io.sentry.compose.gestures.ComposeGestureTargetLocator"; + + static final String COMPOSE_CLASS_NAME = "androidx.compose.ui.node.Owner"; + /** private ctor */ private AndroidOptionsInitializer() {} @@ -143,15 +148,15 @@ static void initializeIntegrationsAndProcessors( if (options.getGestureTargetLocators().isEmpty()) { final List gestureTargetLocators = new ArrayList<>(2); gestureTargetLocators.add(new AndroidViewGestureTargetLocator(isAndroidXScrollViewAvailable)); - try { + + final boolean isComposeUpstreamAvailable = + loadClass.isClassAvailable(COMPOSE_CLASS_NAME, options); + final boolean isComposeAvailable = + (isComposeUpstreamAvailable + && loadClass.isClassAvailable(SENTRY_COMPOSE_INTEGRATION_CLASS_NAME, options)); + + if (isComposeAvailable) { gestureTargetLocators.add(new ComposeGestureTargetLocator()); - } catch (NoClassDefFoundError error) { - options - .getLogger() - .log( - SentryLevel.DEBUG, - "ComposeGestureTargetLocator not available, consider adding the `sentry-compose` library.", - error); } options.setGestureTargetLocators(gestureTargetLocators); } diff --git a/sentry-android-core/src/test/AndroidManifest.xml b/sentry-android-core/src/test/AndroidManifest.xml new file mode 100644 index 0000000000..87fa579cf6 --- /dev/null +++ b/sentry-android-core/src/test/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + 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 cf61131fe3..5b8dadf097 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 @@ -8,12 +8,15 @@ import io.sentry.ILogger import io.sentry.MainEventProcessor import io.sentry.SentryOptions import io.sentry.android.core.cache.AndroidEnvelopeCache +import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator import io.sentry.android.core.internal.modules.AssetsModulesLoader import io.sentry.android.core.internal.util.AndroidMainThreadChecker import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.timber.SentryTimberIntegration +import io.sentry.compose.gestures.ComposeGestureTargetLocator import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import java.io.File @@ -66,7 +69,7 @@ class AndroidOptionsInitializerTest { fun initSutWithClassLoader( minApi: Int = 16, - classToLoad: Class<*>? = null, + classesToLoad: List = emptyList(), isFragmentAvailable: Boolean = false, isTimberAvailable: Boolean = false ) { @@ -89,7 +92,7 @@ class AndroidOptionsInitializerTest { sentryOptions, context, buildInfo, - createClassMock(classToLoad), + createClassMock(classesToLoad), isFragmentAvailable, isTimberAvailable ) @@ -101,10 +104,14 @@ class AndroidOptionsInitializerTest { return buildInfo } - private fun createClassMock(clazz: Class<*>?): LoadClass { + private fun createClassMock(classes: List): LoadClass { val loadClassMock = mock() - whenever(loadClassMock.loadClass(any(), any())).thenReturn(clazz) - whenever(loadClassMock.isClassAvailable(any(), any())).thenReturn(clazz != null) + classes.forEach { + whenever(loadClassMock.loadClass(eq(it), any())) + .thenReturn(Class.forName(it, false, this::class.java.classLoader)) + whenever(loadClassMock.isClassAvailable(eq(it), any())) + .thenReturn(true) + } return loadClassMock } } @@ -267,7 +274,7 @@ class AndroidOptionsInitializerTest { @Test fun `NdkIntegration will load SentryNdk class and add to the integration list`() { - fixture.initSutWithClassLoader(classToLoad = SentryNdk::class.java) + fixture.initSutWithClassLoader(classesToLoad = listOfNotNull(NdkIntegration.SENTRY_NDK_CLASS_NAME)) val actual = fixture.sentryOptions.integrations.firstOrNull { it is NdkIntegration } assertNotNull((actual as NdkIntegration).sentryNdkClass) @@ -275,7 +282,7 @@ class AndroidOptionsInitializerTest { @Test fun `NdkIntegration won't be enabled because API is lower than 16`() { - fixture.initSutWithClassLoader(minApi = 14, classToLoad = SentryNdk::class.java) + fixture.initSutWithClassLoader(minApi = 14, classesToLoad = listOfNotNull(NdkIntegration.SENTRY_NDK_CLASS_NAME)) val actual = fixture.sentryOptions.integrations.firstOrNull { it is NdkIntegration } assertNull((actual as NdkIntegration).sentryNdkClass) @@ -283,7 +290,7 @@ class AndroidOptionsInitializerTest { @Test fun `NdkIntegration won't be enabled, if class not found`() { - fixture.initSutWithClassLoader(classToLoad = null) + fixture.initSutWithClassLoader(classesToLoad = emptyList()) val actual = fixture.sentryOptions.integrations.firstOrNull { it is NdkIntegration } assertNull((actual as NdkIntegration).sentryNdkClass) @@ -455,4 +462,26 @@ class AndroidOptionsInitializerTest { assertTrue { fixture.sentryOptions.mainThreadChecker is AndroidMainThreadChecker } } + + @Test + fun `does not install ComposeGestureTargetLocator, if sentry-compose is not available`() { + fixture.initSutWithClassLoader() + + assertTrue { fixture.sentryOptions.gestureTargetLocators.size == 1 } + assertTrue { fixture.sentryOptions.gestureTargetLocators[0] is AndroidViewGestureTargetLocator } + } + + @Test + fun `installs ComposeGestureTargetLocator, if sentry-compose is available`() { + fixture.initSutWithClassLoader( + classesToLoad = listOf( + AndroidOptionsInitializer.COMPOSE_CLASS_NAME, + AndroidOptionsInitializer.SENTRY_COMPOSE_INTEGRATION_CLASS_NAME + ) + ) + + assertTrue { fixture.sentryOptions.gestureTargetLocators.size == 2 } + assertTrue { fixture.sentryOptions.gestureTargetLocators[0] is AndroidViewGestureTargetLocator } + assertTrue { fixture.sentryOptions.gestureTargetLocators[1] is ComposeGestureTargetLocator } + } } diff --git a/sentry-android-integration-tests/README.md b/sentry-android-integration-tests/README.md index 585fa5fa06..26c4a327f4 100644 --- a/sentry-android-integration-tests/README.md +++ b/sentry-android-integration-tests/README.md @@ -5,3 +5,4 @@ * [App metrics test specification (yaml)](./metrics-test.yml) * [Espresso-based benchmarks](./sentry-uitest-android-benchmark) - run within SauceLabs (see /.sauce/*.yml) * [Espresso-based UI tests](./sentry-uitest-android) - run within SauceLabs (see /.sauce/*.yml) +* [Sample app for testing against AGP compatibility matrix](./sentry-test-agp) diff --git a/sentry-android-integration-tests/sentry-test-agp/.gitignore b/sentry-android-integration-tests/sentry-test-agp/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/sentry-android-integration-tests/sentry-test-agp/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sentry-android-integration-tests/sentry-test-agp/build.gradle.kts b/sentry-android-integration-tests/sentry-test-agp/build.gradle.kts new file mode 100644 index 0000000000..7f6e8d79f1 --- /dev/null +++ b/sentry-android-integration-tests/sentry-test-agp/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + id("com.android.application") +} + +android { + compileSdk = Config.Android.compileSdkVersion + namespace = "io.sentry.test.agp" + + defaultConfig { + applicationId = "io.sentry.test.agp" + minSdk = Config.Android.minSdkVersionOkHttp + targetSdk = Config.Android.targetSdkVersion + versionCode = 1 + versionName = "1.0" + } + + buildTypes { + getByName("release") { + isMinifyEnabled = true + signingConfig = signingConfigs.getByName("debug") // to be able to run release mode + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt") + ) + ndk { + abiFilters.clear() + abiFilters.add("arm64-v8a") + } + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + buildFeatures { + viewBinding = true + } + signingConfigs { + getByName("debug") { + storeFile = rootProject.file("debug.keystore") + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } + } + + variantFilter { + if (Config.Android.shouldSkipDebugVariant(buildType.name)) { + ignore = true + } + } +} +dependencies { + // just a mix of different dependencies to test how our logic for checking classes at runtime + // works with r8 + implementation(projects.sentryAndroid) + implementation(projects.sentryAndroidOkhttp) + implementation(projects.sentryAndroidFragment) + implementation(projects.sentryAndroidTimber) + + implementation(Config.Libs.fragment) + implementation(Config.Libs.timber) + + implementation(Config.Libs.retrofit2) +} diff --git a/sentry-android-integration-tests/sentry-test-agp/proguard-rules.pro b/sentry-android-integration-tests/sentry-test-agp/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/sentry-android-integration-tests/sentry-test-agp/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sentry-android-integration-tests/sentry-test-agp/src/main/AndroidManifest.xml b/sentry-android-integration-tests/sentry-test-agp/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..5ee3c0aa47 --- /dev/null +++ b/sentry-android-integration-tests/sentry-test-agp/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/sentry-android-integration-tests/sentry-test-agp/src/main/java/io/sentry/test/agp/MainActivity.java b/sentry-android-integration-tests/sentry-test-agp/src/main/java/io/sentry/test/agp/MainActivity.java new file mode 100644 index 0000000000..839683d942 --- /dev/null +++ b/sentry-android-integration-tests/sentry-test-agp/src/main/java/io/sentry/test/agp/MainActivity.java @@ -0,0 +1,17 @@ +package io.sentry.test.agp; + +import android.app.Activity; +import android.os.Bundle; + +public class MainActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + helloWorld(); + } + + public void helloWorld() { + System.out.println("¯\\_(ツ)_/¯"); + } +} diff --git a/sentry-compose/proguard-rules.pro b/sentry-compose/proguard-rules.pro index 244282115a..d2c978a746 100644 --- a/sentry-compose/proguard-rules.pro +++ b/sentry-compose/proguard-rules.pro @@ -1,5 +1,9 @@ ##---------------Begin: proguard configuration for Compose ---------- +# The Android SDK checks at runtime if these classes are available via Class.forName +-keep class io.sentry.compose.gestures.ComposeGestureTargetLocator { (...); } +-keepnames interface androidx.compose.ui.node.Owner + # To ensure that stack traces is unambiguous # https://developer.android.com/studio/build/shrink-code#decode-stack-trace -keepattributes LineNumberTable,SourceFile diff --git a/settings.gradle.kts b/settings.gradle.kts index 3d8af33808..bfbce6c7da 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -58,7 +58,8 @@ include( "sentry-android-integration-tests:sentry-uitest-android-benchmark", "sentry-android-integration-tests:sentry-uitest-android", "sentry-android-integration-tests:test-app-plain", - "sentry-android-integration-tests:test-app-sentry" + "sentry-android-integration-tests:test-app-sentry", + "sentry-android-integration-tests:sentry-test-agp" ) gradle.beforeProject {