From e2aca00e646be13eb64fb355aef5ee655c61209c Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Fri, 6 Nov 2026 13:28:24 +0100 Subject: [PATCH 1/4] fix(android): Resolve NullAway findings in sentry-android-core NullAway never ran on sentry-android-core. Enabling it reports 32 errors. Most are fields and return types that are nullable but not annotated, and fields NullAway cannot see as checked because a check happens in a different scope (a lambda, another method) than the dereference. - Annotate the nullable fields and return types, including ViewHierarchyNode.setType and UiElement.equals in the sentry module. - Read options once in lambdas, and pass scopes and options to createStandaloneAppStartTransaction, whose callers already check them. - Compute the device family once instead of through a LazyEvaluator, whose evaluator must not return null. - Create the system events HandlerThread in the branch that uses it, inside the existing try. Co-Authored-By: Claude Opus 5.5 --- .../core/ActivityLifecycleIntegration.java | 12 +++++++----- .../java/io/sentry/android/core/AppState.java | 4 ++-- .../core/DefaultAndroidEventProcessor.java | 8 +++----- .../android/core/FeedbackShakeIntegration.java | 1 + .../SystemEventsBreadcrumbsIntegration.java | 17 +++++++++-------- .../core/ViewHierarchyEventProcessor.java | 2 +- .../android/core/anr/AnrProfileManager.java | 2 +- .../AndroidViewGestureTargetLocator.java | 2 +- .../internal/threaddump/ThreadDumpParser.java | 2 +- .../internal/tombstone/TombstoneParser.java | 1 + .../util/AndroidConnectionStatusProvider.java | 10 +++++----- .../core/AndroidLoggerBatchProcessorTest.kt | 4 ++-- .../core/AndroidMetricsBatchProcessorTest.kt | 4 ++-- .../core/SessionTrackingIntegrationTest.kt | 2 +- .../SystemEventsBreadcrumbsIntegrationTest.kt | 10 +++++----- .../core/anr/AnrProfilingIntegrationTest.kt | 6 +++--- .../util/AndroidConnectionStatusProviderTest.kt | 2 +- .../io/sentry/internal/gestures/UiElement.java | 2 +- .../io/sentry/protocol/ViewHierarchyNode.java | 2 +- 19 files changed, 48 insertions(+), 45 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 505bb84ea2c..252462711fa 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -186,7 +186,7 @@ private void stopPreviousTransactions() { private void startTracing(final @NotNull Activity activity) { WeakReference weakActivity = new WeakReference<>(activity); - if (scopes != null && !isRunningTransactionOrTrace(activity)) { + if (scopes != null && options != null && !isRunningTransactionOrTrace(activity)) { if (!performanceEnabled) { activitiesWithOngoingTransactions.put(activity, NoOpTransaction.getInstance()); if (options.isEnableAutoTraceIdGeneration()) { @@ -309,7 +309,7 @@ private void startTracing(final @NotNull Activity activity) { // Continue either the foreground app.start above or an earlier headless app.start. final @Nullable String continueSentryTrace; final @Nullable String continueBaggage; - if (createStandaloneAppStart) { + if (createStandaloneAppStart && appStartTransaction != null) { continueSentryTrace = appStartTransaction.toSentryTrace().getValue(); final @Nullable BaggageHeader baggageHeader = appStartTransaction.toBaggageHeader(null); continueBaggage = baggageHeader == null ? null : baggageHeader.getValue(); @@ -359,7 +359,7 @@ private void startTracing(final @NotNull Activity activity) { final SpanOptions spanOptions = new SpanOptions(); setSpanOrigin(spanOptions); - if (isAppStart) { + if (isAppStart && coldStart != null) { if (!createStandaloneAppStart && !options.isEnableStandaloneAppStartTracing()) { appStartSpan = transaction.startChild( @@ -1032,7 +1032,7 @@ private void onHeadlessAppStart() { } final @NotNull ITransaction transaction = - createStandaloneAppStartTransaction(startTime, null, false); + createStandaloneAppStartTransaction(scopes, options, startTime, null, false); transaction.finish(SpanStatus.OK, endTime); } @@ -1043,6 +1043,8 @@ private void onHeadlessAppStart() { * for its children and gets a deadline so it stays open until the extended span finishes. */ private @NotNull ITransaction createStandaloneAppStartTransaction( + final @NotNull IScopes scopes, + final @NotNull SentryAndroidOptions options, final @NotNull SentryDate startTime, final @Nullable TracesSamplingDecision samplingDecision, final boolean holdOpenForExtension) { @@ -1121,7 +1123,7 @@ private void onHeadlessAppStart() { metrics.setAppStartSamplingDecision(null); final @NotNull ITransaction transaction = - createStandaloneAppStartTransaction(startTime, samplingDecision, true); + createStandaloneAppStartTransaction(scopes, options, startTime, samplingDecision, true); final SpanOptions spanOptions = new SpanOptions(); setSpanOrigin(spanOptions); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java index 74522f7aaac..24562e3aa49 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java @@ -25,7 +25,7 @@ public final class AppState implements Closeable { private static @NotNull AppState instance = new AppState(); private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); - private volatile LifecycleObserver lifecycleObserver; + private volatile @Nullable LifecycleObserver lifecycleObserver; private MainLooperHandler handler = new MainLooperHandler(); private AppState() {} @@ -49,7 +49,7 @@ public void resetInstance() { @ApiStatus.Internal @TestOnly - public LifecycleObserver getLifecycleObserver() { + public @Nullable LifecycleObserver getLifecycleObserver() { return lifecycleObserver; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index c7ea0e27e4f..75776dee6ae 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -18,7 +18,6 @@ import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; import io.sentry.util.HintUtils; -import io.sentry.util.LazyEvaluator; import io.sentry.util.Objects; import java.util.Collections; import java.util.List; @@ -40,8 +39,7 @@ final class DefaultAndroidEventProcessor implements SentryEventProcessor { private final @NotNull BuildInfoProvider buildInfoProvider; private final @NotNull SentryAndroidOptions options; @TestOnly final @Nullable Future deviceInfoUtil; - private final @NotNull LazyEvaluator deviceFamily = - new LazyEvaluator<>(() -> ContextUtils.getFamily(NoOpLogger.getInstance())); + private final @Nullable String deviceFamily = ContextUtils.getFamily(NoOpLogger.getInstance()); public DefaultAndroidEventProcessor( final @NotNull Context context, @@ -241,7 +239,7 @@ private void setDevice(final @NotNull SentryLogEvent event) { new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.MODEL)); event.setAttribute( "device.family", - new SentryLogEventAttributeValue(SentryAttributeType.STRING, deviceFamily.getValue())); + new SentryLogEventAttributeValue(SentryAttributeType.STRING, deviceFamily)); } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e); } @@ -269,7 +267,7 @@ private void setDevice(final @NotNull SentryMetricsEvent event) { new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.MODEL)); event.setAttribute( "device.family", - new SentryLogEventAttributeValue(SentryAttributeType.STRING, deviceFamily.getValue())); + new SentryLogEventAttributeValue(SentryAttributeType.STRING, deviceFamily)); } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/FeedbackShakeIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/FeedbackShakeIntegration.java index 4405cd19309..d8bf33b23da 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/FeedbackShakeIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/FeedbackShakeIntegration.java @@ -274,6 +274,7 @@ private void startShakeDetection(final @NotNull Activity activity) { final @Nullable WeakReference ref = currentActivityRef; final Activity active = ref != null ? ref.get() : null; final Boolean inBackground = AppState.getInstance().isInBackground(); + final @Nullable SentryAndroidOptions options = this.options; if (active == null || options == null || !enabled diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java index 3f45a9a9fb8..bdadeb0933f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java @@ -66,8 +66,8 @@ public final class SystemEventsBreadcrumbsIntegration private final @NotNull String[] actions; private volatile boolean isClosed = false; private volatile boolean isStopped = false; - private volatile IntentFilter filter = null; - private volatile HandlerThread handlerThread = null; + private volatile @Nullable IntentFilter filter = null; + private volatile @Nullable HandlerThread handlerThread = null; private final @NotNull AtomicBoolean isReceiverRegistered = new AtomicBoolean(false); private final @NotNull AutoClosableReentrantLock receiverLock = new AutoClosableReentrantLock(); // Track previous battery state to avoid duplicate breadcrumbs when values haven't changed @@ -152,12 +152,6 @@ private void registerReceiver( filter.addAction(item); } } - if (customHandler == null && handlerThread == null) { - handlerThread = - new HandlerThread( - "SystemEventsReceiver", Process.THREAD_PRIORITY_BACKGROUND); - handlerThread.start(); - } try { // registerReceiver can throw SecurityException but it's not documented in the // official docs @@ -167,6 +161,12 @@ private void registerReceiver( if (customHandler != null) { handler = customHandler; } else { + if (handlerThread == null) { + handlerThread = + new HandlerThread( + "SystemEventsReceiver", Process.THREAD_PRIORITY_BACKGROUND); + handlerThread.start(); + } handler = new Handler(handlerThread.getLooper()); } ContextUtils.registerReceiver(context, options, receiver, filter, handler); @@ -198,6 +198,7 @@ private void registerReceiver( @SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references private void scheduleUnregisterReceiver() { + final @Nullable SentryAndroidOptions options = this.options; if (options == null) { return; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java index ffd3323e601..590afa3e556 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java @@ -125,7 +125,7 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options) return event; } - public static byte[] snapshotViewHierarchyAsData( + public static @Nullable byte[] snapshotViewHierarchyAsData( @Nullable Activity activity, @NotNull IThreadChecker threadChecker, @NotNull ISerializer serializer, diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfileManager.java b/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfileManager.java index 7e3e1fd514e..06748574919 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfileManager.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfileManager.java @@ -61,7 +61,7 @@ public AnrProfileManager(final @NotNull SentryOptions options, final @NotNull Fi queueFile, new ObjectQueue.Converter() { @Override - public AnrStackTrace from(final byte[] source) throws IOException { + public @Nullable AnrStackTrace from(final byte[] source) throws IOException { // no need to close the streams since they are backed by byte arrays and don't // hold any resources final @NotNull ByteArrayInputStream bis = new ByteArrayInputStream(source); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/AndroidViewGestureTargetLocator.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/AndroidViewGestureTargetLocator.java index 5f6187cd39a..5799f262ecb 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/AndroidViewGestureTargetLocator.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/AndroidViewGestureTargetLocator.java @@ -40,7 +40,7 @@ && isViewScrollable(view, isAndroidXAvailable.getValue())) { return null; } - private UiElement createUiElement(final @NotNull View targetView) { + private @Nullable UiElement createUiElement(final @NotNull View targetView) { final @Nullable String resourceName = ViewUtils.getResourceIdOrNull(targetView); if (resourceName == null) { return null; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java index bfb829ce1a9..3408a5a79f5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java @@ -178,7 +178,7 @@ public void parse(final @NotNull Lines lines) { markThreads(); } - private SentryThread parseThread(final @NotNull Lines lines) { + private @Nullable SentryThread parseThread(final @NotNull Lines lines) { final SentryThread sentryThread = new SentryThread(); final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher(""); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java index b6ac22b12ef..fdbddd3c3b5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java @@ -316,6 +316,7 @@ void extendTo(final @NotNull MemoryMapping mapping) { this.previousOffset = mapping.offset; } + @Nullable DebugImage toDebugImage() { if (buildId.isEmpty()) { return null; 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 62a212e6dcc..a2c17c5ee63 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 @@ -311,16 +311,16 @@ private void updateCacheAndNotifyObservers( private boolean isSignificantChange(@Nullable NetworkCapabilities newCapabilities) { final NetworkCapabilities oldCapabilities = cachedNetworkCapabilities; - // Always significant if transitioning between null and non-null - if ((oldCapabilities == null) != (newCapabilities == null)) { - return true; - } - // If both null, no change if (oldCapabilities == null && newCapabilities == null) { return false; } + // Always significant if transitioning between null and non-null + if (oldCapabilities == null || newCapabilities == null) { + return true; + } + // Check significant capability changes if (hasSignificantCapabilityChanges(oldCapabilities, newCapabilities)) { return true; diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidLoggerBatchProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidLoggerBatchProcessorTest.kt index 18bf67cd11c..8735d5bcebd 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidLoggerBatchProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidLoggerBatchProcessorTest.kt @@ -96,7 +96,7 @@ class AndroidLoggerBatchProcessorTest { val sut = fixture.getSut() sut.close(false) - assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty()) + assertTrue(AppState.getInstance().lifecycleObserver!!.listeners.isEmpty()) } @Test @@ -104,6 +104,6 @@ class AndroidLoggerBatchProcessorTest { val sut = fixture.getSut() sut.close(true) - assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty()) + assertTrue(AppState.getInstance().lifecycleObserver!!.listeners.isEmpty()) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorTest.kt index a965c066279..199533905e4 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorTest.kt @@ -95,7 +95,7 @@ class AndroidMetricsBatchProcessorTest { val sut = fixture.getSut() sut.close(false) - assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty()) + assertTrue(AppState.getInstance().lifecycleObserver!!.listeners.isEmpty()) } @Test @@ -103,6 +103,6 @@ class AndroidMetricsBatchProcessorTest { val sut = fixture.getSut() sut.close(true) - assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty()) + assertTrue(AppState.getInstance().lifecycleObserver!!.listeners.isEmpty()) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index ef920e1d7fe..6c8eaa4d851 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -119,7 +119,7 @@ class SessionTrackingIntegrationTest { private fun setupLifecycle(): LifecycleRegistry { val lifecycle = LifecycleRegistry(ProcessLifecycleOwner.get()) - lifecycle.addObserver(AppState.getInstance().lifecycleObserver) + lifecycle.addObserver(AppState.getInstance().lifecycleObserver!!) return lifecycle } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt index 2505b6f7f7d..60282aca26d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt @@ -387,7 +387,7 @@ class SystemEventsBreadcrumbsIntegrationTest { sut.register(fixture.scopes, fixture.options) assertTrue( - AppState.getInstance().lifecycleObserver.listeners.any { + AppState.getInstance().lifecycleObserver!!.listeners.any { it is SystemEventsBreadcrumbsIntegration } ) @@ -401,7 +401,7 @@ class SystemEventsBreadcrumbsIntegrationTest { sut.register(fixture.scopes, fixture.options) assertFalse( - AppState.getInstance().lifecycleObserver.listeners.any { + AppState.getInstance().lifecycleObserver!!.listeners.any { it is SystemEventsBreadcrumbsIntegration } ) @@ -414,7 +414,7 @@ class SystemEventsBreadcrumbsIntegrationTest { sut.register(fixture.scopes, fixture.options) assertTrue( - AppState.getInstance().lifecycleObserver.listeners.any { + AppState.getInstance().lifecycleObserver!!.listeners.any { it is SystemEventsBreadcrumbsIntegration } ) @@ -422,7 +422,7 @@ class SystemEventsBreadcrumbsIntegrationTest { sut.close() assertFalse( - AppState.getInstance().lifecycleObserver.listeners.any { + AppState.getInstance().lifecycleObserver!!.listeners.any { it is SystemEventsBreadcrumbsIntegration } ) @@ -447,7 +447,7 @@ class SystemEventsBreadcrumbsIntegrationTest { shadowOf(Looper.getMainLooper()).idle() assertFalse( - AppState.getInstance().lifecycleObserver.listeners.any { + AppState.getInstance().lifecycleObserver!!.listeners.any { it is SystemEventsBreadcrumbsIntegration } ) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrProfilingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrProfilingIntegrationTest.kt index 76cdac4d1e6..6c3d08c76bc 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrProfilingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrProfilingIntegrationTest.kt @@ -107,7 +107,7 @@ class AnrProfilingIntegrationTest { val thread = integration.getProperty("thread") assertNotNull(thread) - assertTrue(AppState.getInstance().lifecycleObserver.listeners.isNotEmpty()) + assertTrue(AppState.getInstance().lifecycleObserver!!.listeners.isNotEmpty()) integration.close() thread.join(2000) @@ -115,7 +115,7 @@ class AnrProfilingIntegrationTest { assertTrue(!thread.isAlive) val enabled = integration.getProperty("enabled") assertTrue(!enabled.get()) - assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty()) + assertTrue(AppState.getInstance().lifecycleObserver!!.listeners.isEmpty()) } @Test @@ -354,7 +354,7 @@ class AnrProfilingIntegrationTest { val integration = AnrProfilingIntegration() integration.register(mockScopes, androidOptions) - assertFalse(AppState.getInstance().lifecycleObserver.listeners.isEmpty()) + assertFalse(AppState.getInstance().lifecycleObserver!!.listeners.isEmpty()) assertTrue(SentryIntegrationPackageStorage.getInstance().integrations.contains("AnrProfiling")) integration.close() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt index 56a2a12928c..b8319d49e21 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt @@ -792,7 +792,7 @@ class AndroidConnectionStatusProviderTest { connectionStatusProvider.onBackground() assertFalse( - AppState.getInstance().lifecycleObserver.listeners.any { + AppState.getInstance().lifecycleObserver!!.listeners.any { it is SystemEventsBreadcrumbsIntegration } ) diff --git a/sentry/src/main/java/io/sentry/internal/gestures/UiElement.java b/sentry/src/main/java/io/sentry/internal/gestures/UiElement.java index c675910800a..92846cd8d96 100644 --- a/sentry/src/main/java/io/sentry/internal/gestures/UiElement.java +++ b/sentry/src/main/java/io/sentry/internal/gestures/UiElement.java @@ -51,7 +51,7 @@ public UiElement( } @Override - public boolean equals(Object o) { + public boolean equals(final @Nullable Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UiElement uiElement = (UiElement) o; diff --git a/sentry/src/main/java/io/sentry/protocol/ViewHierarchyNode.java b/sentry/src/main/java/io/sentry/protocol/ViewHierarchyNode.java index 525d644fdc5..97fbea385c0 100644 --- a/sentry/src/main/java/io/sentry/protocol/ViewHierarchyNode.java +++ b/sentry/src/main/java/io/sentry/protocol/ViewHierarchyNode.java @@ -49,7 +49,7 @@ public void setRenderingSystem(String renderingSystem) { this.renderingSystem = renderingSystem; } - public void setType(String type) { + public void setType(final @Nullable String type) { this.type = type; } From ec12504da41a39df215a6b1e1a9cca4254a6aad7 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 10 Nov 2026 08:48:22 +0100 Subject: [PATCH 2/4] ref(android): Build the system events IntentFilter where it is registered The filter was a nullable field, built on the first registration and cleared in close(). It only depends on the final actions array, so build it in the registration task instead. That leaves nothing nullable to track. Co-Authored-By: Claude Opus 5.5 --- .../core/SystemEventsBreadcrumbsIntegration.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java index bdadeb0933f..c203fca8de9 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java @@ -66,7 +66,6 @@ public final class SystemEventsBreadcrumbsIntegration private final @NotNull String[] actions; private volatile boolean isClosed = false; private volatile boolean isStopped = false; - private volatile @Nullable IntentFilter filter = null; private volatile @Nullable HandlerThread handlerThread = null; private final @NotNull AtomicBoolean isReceiverRegistered = new AtomicBoolean(false); private final @NotNull AutoClosableReentrantLock receiverLock = new AutoClosableReentrantLock(); @@ -146,11 +145,9 @@ private void registerReceiver( } receiver = new SystemEventsBroadcastReceiver(scopes, options); - if (filter == null) { - filter = new IntentFilter(); - for (String item : actions) { - filter.addAction(item); - } + final @NotNull IntentFilter filter = new IntentFilter(); + for (String item : actions) { + filter.addAction(item); } try { // registerReceiver can throw SecurityException but it's not documented in the @@ -234,7 +231,6 @@ private void unregisterReceiver(final @NotNull SentryAndroidOptions options) { public void close() throws IOException { try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) { isClosed = true; - filter = null; if (handlerThread != null) { handlerThread.quit(); } From 11f129a4d311f8577fb35d6cda1f918e43001fe5 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 10 Nov 2026 09:31:54 +0100 Subject: [PATCH 3/4] fix(android): Check options before finishing the app start span finishAppStartSpan passed the nullable options field to getAppStartTimeSpanWithFallback, which NullAway reports on main. The end time is only used when performance is enabled, which register() sets together with options, so return early when either is unset. Co-Authored-By: Claude Opus 5.5 --- .../io/sentry/android/core/ActivityLifecycleIntegration.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 252462711fa..09907849b34 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -977,13 +977,16 @@ private void finishAppStartSpan() { } private void finishAppStartSpan(final @Nullable SentryDate endDate) { + if (!performanceEnabled || options == null) { + return; + } final @Nullable SentryDate appStartEndTime = endDate != null ? endDate : AppStartMetrics.getInstance() .getAppStartTimeSpanWithFallback(options) .getProjectedStopTimestamp(); - if (performanceEnabled && appStartEndTime != null) { + if (appStartEndTime != null) { finishSpan(appStartSpan, appStartEndTime); if (appStartTransaction != null && !appStartTransaction.isFinished()) { appStartTransaction.finish(SpanStatus.OK, appStartEndTime); From a61a611ebe44202d5feb42e5ab12b6b42b5167b6 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 11 Nov 2026 11:51:23 +0100 Subject: [PATCH 4/4] fix(android): Annotate the view hierarchy byte array as nullable @Nullable byte[] annotates the element type for TYPE_USE annotations, so Kotlin saw ByteArray instead of ByteArray?. Co-Authored-By: Claude Opus 5.5 --- .../io/sentry/android/core/ViewHierarchyEventProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java index 590afa3e556..fb56d1778fb 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java @@ -125,7 +125,7 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options) return event; } - public static @Nullable byte[] snapshotViewHierarchyAsData( + public static byte @Nullable [] snapshotViewHierarchyAsData( @Nullable Activity activity, @NotNull IThreadChecker threadChecker, @NotNull ISerializer serializer,