From e80ff511ede87b30dd6b53912e11abce8b856ad2 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 10 Nov 2026 14:41:51 +0100 Subject: [PATCH 1/2] build(android): Run Error Prone and NullAway on the Android modules net.ltgt.errorprone dropped Android support in 3.0.0, which we have used since 6.7. Since then it only enables Error Prone and adds it to the processor path for Gradle's JVM source sets. On sentry-android-core and sentry-uitest-android the errorprone dependencies and the NullAway ERROR setting had no effect, and the Java compile tasks ran plain javac. Use the AGP variant API to add the errorprone configuration to each variant's annotation processor configuration and enable Error Prone on its JavaCompile task. Both APIs exist in AGP 9.0.0, the oldest version in the AGP matrix. Pass -XDandroidCompatible=true so Error Prone treats these modules as Android code: it skips checks that don't apply there, such as DefaultCharset, and runs its Android checks. sentry-uitest-android has no hand-written Java, so exclude the view binding classes AGP generates, which trip LabelledBreakTarget. Co-Authored-By: Claude Opus 5.5 --- sentry-android-core/build.gradle.kts | 17 +++++++++++---- .../sentry-uitest-android/build.gradle.kts | 21 ++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index c261c56f78..2c175f4c38 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -74,10 +74,19 @@ android { } } -tasks.withType().configureEach { - options.errorprone { - check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) - option("NullAway:AnnotatedPackages", "io.sentry") +// net.ltgt.errorprone only configures Gradle's JVM source sets, not Android variants, so put Error +// Prone on each variant's processor path and enable it ourselves. +androidComponents.onVariants { variant -> + variant.annotationProcessorConfiguration.extendsFrom(configurations.errorprone.get()) + variant.configureJavaCompileTask { task -> + // Tells Error Prone this is Android code, so it skips checks that don't apply there, such as + // DefaultCharset (Android's default charset is always UTF-8), and runs its Android checks. + task.options.compilerArgs.add("-XDandroidCompatible=true") + task.options.errorprone { + enabled = true + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } } } 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 784bd463c5..557a5fab63 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts @@ -135,11 +135,22 @@ dependencies { androidTestImplementation(libs.okhttp.mockwebserver4) } -tasks.withType().configureEach { - options.errorprone { - check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) - option("NullAway:AnnotatedPackages", "io.sentry") - option("NullAway:UnannotatedSubPackages", "io.sentry.uitest.android.databinding") +// net.ltgt.errorprone only configures Gradle's JVM source sets, not Android variants, so put Error +// Prone on each variant's processor path and enable it ourselves. +androidComponents.onVariants { variant -> + variant.annotationProcessorConfiguration.extendsFrom(configurations.errorprone.get()) + variant.configureJavaCompileTask { task -> + // Tells Error Prone this is Android code, so it skips checks that don't apply there, such as + // DefaultCharset (Android's default charset is always UTF-8), and runs its Android checks. + task.options.compilerArgs.add("-XDandroidCompatible=true") + task.options.errorprone { + enabled = true + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + option("NullAway:UnannotatedSubPackages", "io.sentry.uitest.android.databinding") + // AGP generates the view binding classes, so their findings can't be fixed here. + excludedPaths.set(".*/build/generated/.*") + } } } From 836d5e68dff788e54c42d50d278d92604f358d65 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 11 Nov 2026 15:34:03 +0100 Subject: [PATCH 2/2] fix(android): Resolve NullAway findings in AndroidConnectionStatusProvider NullAway treats AtomicReference.get() as nullable, so the snapshot reads added in #6537 fail once NullAway runs on sentry-android-core. The snapshot is never set to null, so read it through one accessor that suppresses the finding. Co-Authored-By: Claude Opus 5.5 --- .../util/AndroidConnectionStatusProvider.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) 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 256cf88e7b..df8a3b976d 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 @@ -324,7 +324,7 @@ private void ensureNetworkCallbackRegistered() { public void onAvailable(final @NotNull Network network) { @NotNull ConnectionSnapshot current; do { - current = snapshot.get(); + current = currentSnapshot(); } while (!snapshot.compareAndSet(current, current.withNetwork(network))); // Only forward the call on first registration + when the connection is @@ -348,7 +348,7 @@ public void onUnavailable() { @Override public void onLost(final @NotNull Network network) { - if (!network.equals(snapshot.get().network)) { + if (!network.equals(currentSnapshot().network)) { return; } clearCacheAndNotifyObservers(); @@ -376,7 +376,7 @@ private void clearCacheAndNotifyObservers() { @Override public void onCapabilitiesChanged( @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) { - if (!network.equals(snapshot.get().network)) { + if (!network.equals(currentSnapshot().network)) { return; } updateCacheAndNotifyObservers(networkCapabilities); @@ -397,7 +397,7 @@ private void updateCacheAndNotifyObservers( @NotNull ConnectionSnapshot current; @NotNull ConnectionSnapshot updated; do { - current = snapshot.get(); + current = currentSnapshot(); if (!isSignificantChange(current.capabilities, networkCapabilities)) { return; } @@ -482,9 +482,15 @@ private boolean hasSignificantTransportChanges( } } + // NullAway treats AtomicReference.get() as nullable, but the snapshot is never set to null. + @SuppressWarnings("NullAway") + private @NotNull ConnectionSnapshot currentSnapshot() { + return snapshot.get(); + } + /** The current snapshot, refreshed first if it's stale. */ private @NotNull ConnectionSnapshot freshSnapshot() { - final @NotNull ConnectionSnapshot current = snapshot.get(); + final @NotNull ConnectionSnapshot current = currentSnapshot(); return current.isFresh() ? current : refresh(current); } @@ -510,7 +516,7 @@ private boolean hasSignificantTransportChanges( if (snapshot.compareAndSet(current, refreshed)) { break; } - current = snapshot.get(); + current = currentSnapshot(); if (current.isFresh()) { return current; } @@ -651,7 +657,7 @@ public void onForeground() { // proactively update cache and notify observers on foreground to ensure connectivity // state is not stale - final @NotNull ConnectionStatus status = statusOf(refresh(snapshot.get())); + final @NotNull ConnectionStatus status = statusOf(refresh(currentSnapshot())); if (status == ConnectionStatus.DISCONNECTED) { // onLost is not called retroactively when we registerNetworkCallback (as opposed to // onAvailable), so we have to do it manually for the DISCONNECTED case @@ -699,7 +705,7 @@ public void onBackground() { @TestOnly @Nullable public NetworkCapabilities getCachedNetworkCapabilities() { - return snapshot.get().capabilities; + return currentSnapshot().capabilities; } /**