From 78991a289e37d646c0dff005ff917cec2789dc2e Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:52:52 +0300 Subject: [PATCH 01/14] fix(ui): register the chat back handler once instead of per recomposition The chat branch of OnboardingFlowScreen built an OnBackPressedCallback and called addCallback(this, ...) from inside a composable body. Composable bodies re-run on recomposition, and that function observes eight MainViewModel StateFlows, so every Bluetooth, location, or loading change registered another callback. They were bound to the activity, so none were released until onDestroy. Each accumulated callback also multiplied the work of an unhandled Back press: the callback disabled itself, re-dispatched, and the next one down consulted ChatViewModel again. Five recompositions meant one Back press consulted it five times before reaching the system. BackHandler keeps a single registration across recompositions and disposes it when the branch leaves composition. Driving its enabled flag from ChatState.canHandleBack also retires the disable/re-dispatch/re-enable dance: with nothing to unwind the handler is simply disabled, so the dispatcher falls through to the system. canHandleBack mirrors the branches of ChatViewModel.handleBackPressed and is covered by unit tests, since the handler being enabled and the press being consumed have to agree. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26 --- .../java/com/bitchat/android/MainActivity.kt | 24 ++--- .../java/com/bitchat/android/ui/ChatState.kt | 22 +++++ .../com/bitchat/android/ui/ChatViewModel.kt | 1 + .../android/ui/ChatStateBackNavigationTest.kt | 93 +++++++++++++++++++ 4 files changed, 123 insertions(+), 17 deletions(-) create mode 100644 app/src/test/kotlin/com/bitchat/android/ui/ChatStateBackNavigationTest.kt diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 6f80052a..8f0b84c1 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -4,7 +4,7 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.util.Log -import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels @@ -313,23 +313,13 @@ class MainActivity : OrientationAwareActivity() { } OnboardingState.CHECKING, OnboardingState.INITIALIZING, OnboardingState.COMPLETE -> { - // Set up back navigation handling for the chat screen - val backCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - // Let ChatViewModel handle navigation state - val handled = chatViewModel.handleBackPressed() - if (!handled) { - // If ChatViewModel doesn't handle it, disable this callback - // and let the system handle it (which will exit the app) - this.isEnabled = false - onBackPressedDispatcher.onBackPressed() - this.isEnabled = true - } - } + // Intercept Back only while the chat has navigation state to + // unwind. Staying disabled otherwise lets the dispatcher fall + // through to the system, which exits the app. + val canHandleBack by chatViewModel.canHandleBack.collectAsState() + BackHandler(enabled = canHandleBack) { + chatViewModel.handleBackPressed() } - - // Add the callback - this will be automatically removed when the activity is destroyed - onBackPressedDispatcher.addCallback(this, backCallback) ChatScreen(viewModel = chatViewModel) } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatState.kt b/app/src/main/java/com/bitchat/android/ui/ChatState.kt index f4504cc2..e0790894 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatState.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatState.kt @@ -170,6 +170,28 @@ class ChatState( initialValue = false ) + // True while some in-app navigation state is open for Back to unwind. + // Mirrors the branches of ChatViewModel.handleBackPressed: the back + // handler is enabled from this, the press is consumed by that, and the + // two drifting apart is what makes Back feel broken. + val canHandleBack: StateFlow = combine( + _showAppInfo, + _showPasswordPrompt, + _selectedPrivateChatPeer, + _privateChatSheetPeer, + _currentChannel + ) { showAppInfo, showPasswordPrompt, privateChatPeer, privateChatSheetPeer, channel -> + showAppInfo || + showPasswordPrompt || + privateChatPeer != null || + privateChatSheetPeer != null || + channel != null + }.stateIn( + scope = scope, + started = WhileSubscribed(5_000), + initialValue = false + ) + // Getters for internal state access fun getMessagesValue() = _messages.value fun getConnectedPeersValue() = _connectedPeers.value diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index bf9e2f7c..76e784bc 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -377,6 +377,7 @@ class ChatViewModel( val passwordPromptChannel: StateFlow = state.passwordPromptChannel val hasUnreadChannels = state.hasUnreadChannels val hasUnreadPrivateMessages = state.hasUnreadPrivateMessages + val canHandleBack = state.canHandleBack val showCommandSuggestions: StateFlow = state.showCommandSuggestions val commandSuggestions: StateFlow> = state.commandSuggestions val showMentionSuggestions: StateFlow = state.showMentionSuggestions diff --git a/app/src/test/kotlin/com/bitchat/android/ui/ChatStateBackNavigationTest.kt b/app/src/test/kotlin/com/bitchat/android/ui/ChatStateBackNavigationTest.kt new file mode 100644 index 00000000..b0bacbd8 --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/ui/ChatStateBackNavigationTest.kt @@ -0,0 +1,93 @@ +package com.bitchat.android.ui + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +/** + * `canHandleBack` tells the chat screen's back handler whether there is any + * in-app navigation state left to unwind. It has to agree with the branches of + * [ChatViewModel.handleBackPressed], because the handler is enabled from one + * and the press is consumed by the other. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class ChatStateBackNavigationTest { + + private lateinit var scope: TestScope + private lateinit var state: ChatState + private lateinit var subscription: Job + + @Before + fun setUp() { + scope = TestScope(UnconfinedTestDispatcher()) + state = ChatState(scope) + // canHandleBack is shared WhileSubscribed, so it only tracks its + // sources while something collects it. The composable does that in + // production; the test has to do it explicitly. + subscription = scope.launch { state.canHandleBack.collect { } } + } + + @After + fun tearDown() { + subscription.cancel() + } + + @Test + fun `is false on the bare chat screen`() { + assertFalse(state.canHandleBack.value) + } + + @Test + fun `is true while the app info dialog is open`() { + state.setShowAppInfo(true) + + assertTrue(state.canHandleBack.value) + } + + @Test + fun `is true while the password prompt is open`() { + state.setShowPasswordPrompt(true) + + assertTrue(state.canHandleBack.value) + } + + @Test + fun `is true while a private chat is selected`() { + state.setSelectedPrivateChatPeer("peer-a") + + assertTrue(state.canHandleBack.value) + } + + @Test + fun `is true while the private chat sheet is open`() { + state.setPrivateChatSheetPeer("peer-a") + + assertTrue(state.canHandleBack.value) + } + + @Test + fun `is true while a channel is open`() { + state.setCurrentChannel("#bitchat") + + assertTrue(state.canHandleBack.value) + } + + @Test + fun `returns to false once the last overlay closes`() { + state.setCurrentChannel("#bitchat") + state.setShowAppInfo(true) + + state.setShowAppInfo(false) + assertTrue("the channel is still open", state.canHandleBack.value) + + state.setCurrentChannel(null) + assertFalse("nothing is left to unwind", state.canHandleBack.value) + } +} From 68361d949f4bcc6eae06005b7e321c8cc0d4577c Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:11:54 +0300 Subject: [PATCH 02/14] fix(ui): forward Back when canHandleBack is momentarily stale BackHandler's enabled flag trails the state it mirrors by a coroutine dispatch and a recomposition. A second Back press inside that window finds the handler still enabled while handleBackPressed() already has nothing to unwind, and ignoring its result consumed the press instead of letting the system act on it. The callback the handler replaced did forward it. Falls back to finish() on an unhandled press, which is what the dispatcher reached before once no enabled callback consumed it. Reported by Codex review on #912. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26 --- app/src/main/java/com/bitchat/android/MainActivity.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 8f0b84c1..aa336810 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -318,7 +318,11 @@ class MainActivity : OrientationAwareActivity() { // through to the system, which exits the app. val canHandleBack by chatViewModel.canHandleBack.collectAsState() BackHandler(enabled = canHandleBack) { - chatViewModel.handleBackPressed() + // enabled reaches this handler a dispatch and a recomposition + // after the state changes, so a second press can arrive while + // it is still true but there is no longer anything to unwind. + // Forward that press instead of swallowing it. + if (!chatViewModel.handleBackPressed()) finish() } ChatScreen(viewModel = chatViewModel) } From 11097a629a19637e8df3aa5aec40e7e1ad6777f4 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:28:55 +0300 Subject: [PATCH 03/14] style: cut the commentary around the back handler down to what the code cannot say The block carried seven comment lines over four of code, against 0.17 for the file as a whole. Most of it restated the identifiers: enabled = canHandleBack does not need a sentence explaining that Back is intercepted while there is state to unwind. What is left is the part that is not derivable. That enabled is a variable rather than true is a predictive-back decision, and finish() being reachable at all only makes sense once you know the flag trails the state it mirrors. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26 --- app/src/main/java/com/bitchat/android/MainActivity.kt | 10 +++------- app/src/main/java/com/bitchat/android/ui/ChatState.kt | 6 ++---- .../bitchat/android/ui/ChatStateBackNavigationTest.kt | 11 ++++------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index aa336810..3d752291 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -313,15 +313,11 @@ class MainActivity : OrientationAwareActivity() { } OnboardingState.CHECKING, OnboardingState.INITIALIZING, OnboardingState.COMPLETE -> { - // Intercept Back only while the chat has navigation state to - // unwind. Staying disabled otherwise lets the dispatcher fall - // through to the system, which exits the app. val canHandleBack by chatViewModel.canHandleBack.collectAsState() + // Disabled rather than always-on so predictive back can preview + // the exit instead of the app claiming every gesture. BackHandler(enabled = canHandleBack) { - // enabled reaches this handler a dispatch and a recomposition - // after the state changes, so a second press can arrive while - // it is still true but there is no longer anything to unwind. - // Forward that press instead of swallowing it. + // enabled trails the state by a dispatch and a recomposition. if (!chatViewModel.handleBackPressed()) finish() } ChatScreen(viewModel = chatViewModel) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatState.kt b/app/src/main/java/com/bitchat/android/ui/ChatState.kt index e0790894..16210894 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatState.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatState.kt @@ -170,10 +170,8 @@ class ChatState( initialValue = false ) - // True while some in-app navigation state is open for Back to unwind. - // Mirrors the branches of ChatViewModel.handleBackPressed: the back - // handler is enabled from this, the press is consumed by that, and the - // two drifting apart is what makes Back feel broken. + // Mirrors the branches of ChatViewModel.handleBackPressed. The back handler + // is enabled from this and the press consumed by that, so they must agree. val canHandleBack: StateFlow = combine( _showAppInfo, _showPasswordPrompt, diff --git a/app/src/test/kotlin/com/bitchat/android/ui/ChatStateBackNavigationTest.kt b/app/src/test/kotlin/com/bitchat/android/ui/ChatStateBackNavigationTest.kt index b0bacbd8..5c5c41de 100644 --- a/app/src/test/kotlin/com/bitchat/android/ui/ChatStateBackNavigationTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/ui/ChatStateBackNavigationTest.kt @@ -12,10 +12,9 @@ import org.junit.Before import org.junit.Test /** - * `canHandleBack` tells the chat screen's back handler whether there is any - * in-app navigation state left to unwind. It has to agree with the branches of - * [ChatViewModel.handleBackPressed], because the handler is enabled from one - * and the press is consumed by the other. + * `canHandleBack` must agree with the branches of + * [ChatViewModel.handleBackPressed]: the back handler is enabled from one and + * the press consumed by the other. */ @OptIn(ExperimentalCoroutinesApi::class) class ChatStateBackNavigationTest { @@ -28,9 +27,7 @@ class ChatStateBackNavigationTest { fun setUp() { scope = TestScope(UnconfinedTestDispatcher()) state = ChatState(scope) - // canHandleBack is shared WhileSubscribed, so it only tracks its - // sources while something collects it. The composable does that in - // production; the test has to do it explicitly. + // Shared WhileSubscribed, so it only tracks its sources while collected. subscription = scope.launch { state.canHandleBack.collect { } } } From e6db3ec7e479340c0eddb74528abd4d1e3c49ba9 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Sat, 15 Aug 2026 01:29:19 +0300 Subject: [PATCH 04/14] build: add KSP and Hilt to the phone client Adds the KSP and Hilt Gradle plugins and the Hilt runtime/compiler dependencies to :app, with the lock state and verification metadata regenerated per docs/reproducible-builds.md. No source changes; nothing is injected yet. KSP versions independently of Kotlin as of 2.3.0. Earlier releases used a - scheme requiring an exact match to the Kotlin version, so the catalog records the reason the two versions no longer track. Verification metadata gains 36 components and removes none. Reviewed: - androidx.compose.runtime:runtime:1.8.2 and androidx.annotation:annotation-experimental:1.3.1 are .module metadata only, with no jar or aar. They are resolution metadata, not artifacts, and do not downgrade what the Compose BOM selects. - kotlin-stdlib:2.3.20 carries a jar but the lock state confines it to kspPluginClasspath, so it never reaches a runtime classpath. - com.google.dagger:dagger was already on the runtime classpath at 2.59 transitively; it moves to 2.60.1. - The remainder are the Dagger compiler's own build-time dependencies (guava, kotlinpoet, javapoet, google-java-format, error_prone). wear/gradle.lockfile and settings-gradle.lockfile are unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NaZivRfLu7FdKjzVwwRjcc (cherry picked from commit a97675b46527f98f5493150b53c926caa5115058) --- app/build.gradle.kts | 9 +- app/gradle.lockfile | 45 ++++-- build.gradle.kts | 2 + gradle/libs.versions.toml | 14 ++ gradle/verification-metadata.xml | 267 +++++++++++++++++++++++++++++++ 5 files changed, 326 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 26042d6e..939c1b33 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,6 +4,8 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) } val githubReleaseCertSha256 = providers @@ -141,7 +143,12 @@ dependencies { // Navigation implementation(libs.androidx.navigation.compose) - + + // Dependency injection + implementation(libs.hilt.android) + implementation(libs.androidx.hilt.navigation.compose) + ksp(libs.hilt.compiler) + // Permissions implementation(libs.accompanist.permissions) diff --git a/app/gradle.lockfile b/app/gradle.lockfile index c66fac1d..21a4ec95 100644 --- a/app/gradle.lockfile +++ b/app/gradle.lockfile @@ -93,6 +93,9 @@ androidx.emoji2:emoji2:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTes androidx.exifinterface:exifinterface:1.4.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.fragment:fragment:1.5.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.graphics:graphics-path:1.0.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.hilt:hilt-lifecycle-viewmodel-compose:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.hilt:hilt-lifecycle-viewmodel:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.hilt:hilt-navigation-compose:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.interpolator:interpolator:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.legacy:legacy-support-core-utils:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.lifecycle:lifecycle-common-java8:2.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath @@ -236,33 +239,46 @@ com.google.auto.service:auto-service:1.1.1=unified-test-platform-android-device- com.google.auto.value:auto-value-annotations:1.11.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath com.google.auto.value:auto-value-annotations:1.6.3=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.auto:auto-common:1.2.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle -com.google.code.findbugs:jsr305:3.0.2=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.code.findbugs:jsr305:3.0.2=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher com.google.code.gson:gson:2.10.1=unified-test-platform-core,unified-test-platform-gradle-work-action com.google.code.gson:gson:2.11.0=androidLintTool,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-result-listener-gradle com.google.code.gson:gson:2.14.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.code.gson:gson:2.8.9=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher com.google.crypto.tink:tink-android:1.23.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.crypto.tink:tink:1.18.0=unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action +com.google.dagger:dagger-compiler:2.60.1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.dagger:dagger-lint-aar:2.60.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.dagger:dagger-spi:2.60.1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath com.google.dagger:dagger:2.48=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action -com.google.dagger:dagger:2.59=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.dagger:dagger:2.60.1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.dagger:hilt-android-compiler:2.60.1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.dagger:hilt-android:2.60.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.dagger:hilt-compiler:2.60.1=hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease +com.google.dagger:hilt-core:2.60.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.devtools.ksp:symbol-processing-api:2.3.11=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kspPluginClasspath,kspPluginClasspathNonEmbeddable +com.google.devtools.ksp:symbol-processing-api:2.3.7=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath com.google.errorprone:error_prone_annotation:2.38.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath com.google.errorprone:error_prone_annotations:2.23.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher com.google.errorprone:error_prone_annotations:2.36.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.errorprone:error_prone_annotations:2.47.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath com.google.errorprone:error_prone_annotations:2.48.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.firebase:firebase-annotations:16.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.firebase:firebase-components:16.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.firebase:firebase-encoders-json:17.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.firebase:firebase-encoders:16.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.googlejavaformat:google-java-format:1.33.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath com.google.guava:failureaccess:1.0.1=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher com.google.guava:failureaccess:1.0.2=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action -com.google.guava:failureaccess:1.0.3=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.guava:failureaccess:1.0.3=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath com.google.guava:guava:32.0.1-jre=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher com.google.guava:guava:33.4.0-jre=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action com.google.guava:guava:33.4.8-jre=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.guava:guava:33.6.0-jre=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath com.google.guava:listenablefuture:1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=androidLintTool,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher com.google.j2objc:j2objc-annotations:2.8=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.j2objc:j2objc-annotations:3.1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath com.google.jimfs:jimfs:1.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.google.mlkit:barcode-scanning-common:17.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.mlkit:barcode-scanning:17.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath @@ -288,7 +304,9 @@ com.squareup.okhttp3:okhttp-android:5.4.0=debugAndroidTestCompileClasspath,debug com.squareup.okhttp3:okhttp:5.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.squareup.okio:okio-jvm:3.17.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.squareup.okio:okio:3.17.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup:javapoet:1.13.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath com.squareup:javawriter:2.1.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +com.squareup:kotlinpoet:1.11.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath com.sun.istack:istack-commons-runtime:3.0.8=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.sun.xml.fastinfoset:FastInfoset:1.2.16=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle commons-codec:commons-codec:1.17.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle @@ -337,15 +355,16 @@ io.opencensus:opencensus-proto:0.2.0=unified-test-platform-core,unified-test-pla io.perfmark:perfmark-api:0.26.0=unified-test-platform-core io.perfmark:perfmark-api:0.27.0=unified-test-platform-android-test-plugin-host-emulator-control jakarta.activation:jakarta.activation-api:1.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle -jakarta.inject:jakarta.inject-api:2.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle javax.annotation:javax.annotation-api:1.3.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,unified-test-platform-android-test-plugin-host-emulator-control -javax.inject:javax.inject:1=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action +javax.inject:javax.inject:1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action junit:junit:4.13.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.17.7=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath net.bytebuddy:byte-buddy:1.17.7=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath net.java.dev.jna:jna-platform:5.6.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action net.java.dev.jna:jna:5.6.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +net.ltgt.gradle.incap:incap:0.2=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath net.sf.kxml:kxml2:2.3.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action no.nordicsemi.android:ble:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.apache.commons:commons-compress:1.27.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle @@ -357,6 +376,7 @@ org.bouncycastle:bcpkix-jdk18on:1.79=androidLintTool,unified-test-platform-andro org.bouncycastle:bcprov-jdk18on:1.79=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.bouncycastle:bcprov-jdk18on:1.85=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.bouncycastle:bcutil-jdk18on:1.79=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.checkerframework:checker-compat-qual:2.5.3=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath org.checkerframework:checker-qual:3.33.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher org.checkerframework:checker-qual:3.43.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action org.codehaus.groovy:groovy:3.0.22=androidLintTool @@ -385,9 +405,10 @@ org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspa org.jetbrains.kotlin:kotlin-daemon-client:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease +org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.21=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath org.jetbrains.kotlin:kotlin-parcelize-compiler:2.4.10=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease org.jetbrains.kotlin:kotlin-parcelize-runtime:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugRuntimeClasspathCopy,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseRuntimeClasspathCopy -org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease +org.jetbrains.kotlin:kotlin-reflect:1.6.10=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath org.jetbrains.kotlin:kotlin-reflect:1.8.21=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlin:kotlin-reflect:2.1.20=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath org.jetbrains.kotlin:kotlin-reflect:2.2.10=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle @@ -397,9 +418,11 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21=unified-test-platform-android-t org.jetbrains.kotlin:kotlin-stdlib-common:1.9.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher org.jetbrains.kotlin:kotlin-stdlib-common:2.2.10=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-gradle-work-action org.jetbrains.kotlin:kotlin-stdlib-common:2.4.10=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action @@ -407,6 +430,8 @@ org.jetbrains.kotlin:kotlin-stdlib:1.8.21=unified-test-platform-android-test-plu org.jetbrains.kotlin:kotlin-stdlib:1.9.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action org.jetbrains.kotlin:kotlin-stdlib:2.2.21=composeMappingProducerClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.3.20=kspPluginClasspath,kspPluginClasspathNonEmbeddable +org.jetbrains.kotlin:kotlin-stdlib:2.3.21=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath org.jetbrains.kotlin:kotlin-stdlib:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugRuntimeClasspathCopy,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseRuntimeClasspathCopy org.jetbrains.kotlin:kotlin-tooling-core:2.4.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath @@ -430,9 +455,9 @@ org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0=debugAndroidTestCompileClas org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains:annotations:13.0=composeMappingProducerClasspath,debugRuntimeClasspathCopy,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,releaseRuntimeClasspathCopy +org.jetbrains:annotations:13.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,composeMappingProducerClasspath,debugRuntimeClasspathCopy,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kspDebugKotlinProcessorClasspath,kspPluginClasspath,kspPluginClasspathNonEmbeddable,kspReleaseKotlinProcessorClasspath,releaseRuntimeClasspathCopy org.jetbrains:annotations:23.0.0=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher -org.jspecify:jspecify:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jspecify:jspecify:1.0.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jvnet.staxex:stax-ex:1.8.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.mockito.kotlin:mockito-kotlin:6.3.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath org.mockito:mockito-core:5.23.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath @@ -461,4 +486,4 @@ org.robolectric:shadows-framework:4.15=debugUnitTestCompileClasspath,debugUnitTe org.robolectric:utils-reflector:4.15=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath org.robolectric:utils:4.15=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath org.yaml:snakeyaml:2.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -empty=androidApis,androidJdkImage,androidTestUtil,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugReverseMetadataValues,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,kotlinCompilerPluginClasspath,lintChecks,lintPublish,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata,releaseReverseMetadataValues +empty=_agp_internal_debugAndroidTest_kspClasspath,_agp_internal_debugUnitTest_kspClasspath,androidApis,androidJdkImage,androidTestUtil,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugReverseMetadataValues,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,hiltCompileOnlyDebug,hiltCompileOnlyDebugAndroidTest,hiltCompileOnlyDebugUnitTest,hiltCompileOnlyRelease,kotlinCompilerPluginClasspath,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,lintChecks,lintPublish,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata,releaseReverseMetadataValues diff --git a/build.gradle.kts b/build.gradle.kts index 16bf6428..abe4f5a6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,8 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.hilt) apply false } val resolveIdeRuntimeClasspathCopyLocks = tasks.register("resolveIdeRuntimeClasspathCopyLocks") { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 405f6dc3..92034c89 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,9 @@ # Android and Kotlin agp = "9.3.1" kotlin = "2.4.10" +# KSP versions independently of Kotlin as of 2.3.0; earlier releases used a +# - scheme that had to match the Kotlin version exactly. +ksp = "2.3.11" compileSdk = "37" minSdk = "26" # API 26 for proper BLE support targetSdk = "37" @@ -20,6 +23,10 @@ compose-icons-extended = "1.7.8" # Navigation navigation-compose = "2.9.8" +# Dependency injection +hilt = "2.60.1" +androidx-hilt = "1.4.0" + # Accompanist accompanist-permissions = "0.37.3" @@ -98,6 +105,11 @@ androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle- # Navigation androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } +# Dependency injection +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } +androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-hilt" } + # Accompanist accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist-permissions" } @@ -166,6 +178,8 @@ android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } kotlin-parcelize = { id = "kotlin-parcelize" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } [bundles] compose = [ diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6c6f2a76..8685436f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -59,6 +59,11 @@ + + + + + @@ -549,6 +554,11 @@ + + + + + @@ -1282,6 +1292,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -2822,6 +2856,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2876,6 +3032,14 @@ + + + + + + + + @@ -2929,6 +3093,11 @@ + + + + + @@ -2974,6 +3143,19 @@ + + + + + + + + + + + + + @@ -3027,6 +3209,14 @@ + + + + + + + + @@ -3089,6 +3279,14 @@ + + + + + + + + @@ -3349,6 +3547,14 @@ + + + + + + + + @@ -3904,6 +4110,14 @@ + + + + + + + + @@ -4091,6 +4305,14 @@ + + + + + + + + @@ -4277,6 +4499,11 @@ + + + + + @@ -4482,6 +4709,14 @@ + + + + + + + + @@ -4594,6 +4829,14 @@ + + + + + + + + @@ -4644,6 +4887,14 @@ + + + + + + + + @@ -4668,6 +4919,14 @@ + + + + + + + + @@ -4803,6 +5062,14 @@ + + + + + + + + From 8b7af243b374984bda5504e4d7c8a2e7495b2042 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:05:30 +0300 Subject: [PATCH 05/14] build: raise Gradle metaspace to 1g KSP and the Dagger/Hilt annotation processors load enough additional classes into the daemon that a combined testDebugUnitTest + lintDebug run exhausts the previous 512m ceiling. The failure is worth describing because it does not name its cause: it surfaces as an OutOfMemoryError inside ClassLoader.defineClass in whichever task happens to be loading classes at the time, including lint on an unrelated module. When it lands during a --write-verification-metadata run it can also truncate gradle/verification-metadata.xml to zero bytes, which then fails the next build with "Unable to read dependency verification metadata: Premature end of file". JVM arguments are not build inputs, so this does not affect reproducibility. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NaZivRfLu7FdKjzVwwRjcc --- gradle.properties | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index e3461bd0..5ff2de48 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,8 +27,14 @@ kotlin.code.style=official # signed by an unexpected publisher. BITCHAT_GITHUB_RELEASE_CERT_SHA256=3b03fa66a5451321100792f5b55a7b4966d5c8dc10c6daa40aa95ea489531bca -# JVM heap size configuration to prevent OutOfMemoryError -org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError +# JVM heap size configuration to prevent OutOfMemoryError. +# Metaspace is 1g rather than 512m: KSP and the Hilt/Dagger annotation +# processors load a large amount of extra classes into the daemon, and at 512m +# a combined test + lint + KSP invocation across the modules exhausts it. The +# failure surfaces as ClassLoader.defineClass OutOfMemoryError in unrelated +# tasks, and can truncate gradle/verification-metadata.xml if it lands during +# a --write-verification-metadata run. +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError # Enabled parallel sync for Gradle 9.4+ org.gradle.tooling.parallel=true From bf782f9072e2fa39db582fbe5d03093b74d87904 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Sat, 15 Aug 2026 01:35:16 +0300 Subject: [PATCH 06/14] feat(di): inject the phone client's ViewModels with Hilt Replaces the hand-rolled ViewModelProvider.Factory in MainActivity with constructor injection. No singleton is converted and no behaviour changes; this only moves ViewModel construction into the graph so feature modules can obtain their own ViewModels later. MeshModule bridges the existing process-wide mesh instances into the graph. Its bindings are deliberately unscoped: MeshServiceHolder is the source of truth, it is shared with the foreground service, and it supports replacing the mesh service after a panic clear. A @Singleton binding would pin the first instance for the process lifetime and hand out a stale service after a replacement. ChatViewModel still resolves to a single activity-scoped instance, so the unifiedMeshService.delegate assignment continues to reference the same object it did before. ApkDownloadViewModel is injected through its secondary constructor. Its collaborators are internal types, and routing those through Dagger's generated Java buys nothing; the primary constructor stays available so tests can keep substituting fakes. Verified: :app:assembleDebug, :wear:assembleDebug, and 591 unit tests pass. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NaZivRfLu7FdKjzVwwRjcc (cherry picked from commit 33d6b36e220810c87595851ece7e9a63535c61b0) --- .../com/bitchat/android/BitchatApplication.kt | 2 ++ .../java/com/bitchat/android/MainActivity.kt | 14 +++----- .../java/com/bitchat/android/MainViewModel.kt | 5 ++- .../java/com/bitchat/android/di/MeshModule.kt | 36 +++++++++++++++++++ .../android/hotspot/HotspotActivity.kt | 2 ++ .../android/hotspot/HotspotViewModel.kt | 7 +++- .../java/com/bitchat/android/ui/AboutSheet.kt | 4 +-- .../android/ui/ApkDownloadViewModel.kt | 8 +++++ .../com/bitchat/android/ui/ChatViewModel.kt | 5 ++- 9 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/di/MeshModule.kt diff --git a/app/src/main/java/com/bitchat/android/BitchatApplication.kt b/app/src/main/java/com/bitchat/android/BitchatApplication.kt index 7b5926ef..b4107ba3 100644 --- a/app/src/main/java/com/bitchat/android/BitchatApplication.kt +++ b/app/src/main/java/com/bitchat/android/BitchatApplication.kt @@ -4,10 +4,12 @@ import android.app.Application import com.bitchat.android.nostr.RelayDirectory import com.bitchat.android.ui.theme.ThemePreferenceManager import com.bitchat.android.net.ArtiTorManager +import dagger.hilt.android.HiltAndroidApp /** * Main application class for bitchat Android */ +@HiltAndroidApp class BitchatApplication : Application() { override fun onCreate() { diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 3d752291..41d95a1e 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.Lifecycle import com.bitchat.android.mesh.BluetoothMeshService @@ -46,9 +45,11 @@ import com.bitchat.android.ui.theme.BitchatTheme import com.bitchat.android.wifiaware.WifiAwareController import com.bitchat.android.nostr.PoWPreferenceManager import com.bitchat.android.services.VerificationService +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch +@AndroidEntryPoint class MainActivity : OrientationAwareActivity() { private lateinit var permissionManager: PermissionManager @@ -62,15 +63,8 @@ class MainActivity : OrientationAwareActivity() { private lateinit var unifiedMeshService: MeshService private val mainViewModel: MainViewModel by viewModels() private var pendingMeshForegroundServiceStart = false - private val chatViewModel: ChatViewModel by viewModels { - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return ChatViewModel(application, meshService, unifiedMeshService) as T - } - } - } - + private val chatViewModel: ChatViewModel by viewModels() + private val forceFinishReceiver = object : android.content.BroadcastReceiver() { override fun onReceive(context: android.content.Context, intent: android.content.Intent) { if (intent.action == com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH) { diff --git a/app/src/main/java/com/bitchat/android/MainViewModel.kt b/app/src/main/java/com/bitchat/android/MainViewModel.kt index 15ec6fda..bd55ce02 100644 --- a/app/src/main/java/com/bitchat/android/MainViewModel.kt +++ b/app/src/main/java/com/bitchat/android/MainViewModel.kt @@ -7,9 +7,12 @@ import com.bitchat.android.onboarding.OnboardingState import com.bitchat.android.onboarding.BatteryOptimizationStatus import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject -class MainViewModel : ViewModel() { +@HiltViewModel +class MainViewModel @Inject constructor() : ViewModel() { private val _onboardingState = MutableStateFlow(OnboardingState.CHECKING) val onboardingState: StateFlow = _onboardingState.asStateFlow() diff --git a/app/src/main/java/com/bitchat/android/di/MeshModule.kt b/app/src/main/java/com/bitchat/android/di/MeshModule.kt new file mode 100644 index 00000000..2cb8453c --- /dev/null +++ b/app/src/main/java/com/bitchat/android/di/MeshModule.kt @@ -0,0 +1,36 @@ +package com.bitchat.android.di + +import android.content.Context +import com.bitchat.android.mesh.BluetoothMeshService +import com.bitchat.android.mesh.MeshService +import com.bitchat.android.service.MeshServiceHolder +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent + +/** + * Bridges the process-wide mesh instances into the injection graph. + * + * [MeshServiceHolder] remains the single source of truth: it is shared with the + * foreground service, it synchronises creation, and it supports replacing the + * mesh service after a panic clear. These bindings are therefore deliberately + * unscoped — each injection re-reads the holder. Caching them with @Singleton + * would pin the first instance for the process lifetime and hand out a stale + * mesh service after a replacement. + */ +@Module +@InstallIn(SingletonComponent::class) +object MeshModule { + + @Provides + fun provideBluetoothMeshService( + @ApplicationContext context: Context + ): BluetoothMeshService = MeshServiceHolder.getOrCreate(context) + + @Provides + fun provideMeshService( + @ApplicationContext context: Context + ): MeshService = MeshServiceHolder.getUnifiedOrCreate(context) +} diff --git a/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt b/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt index 35d0f4ec..8f894f2d 100644 --- a/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt +++ b/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt @@ -7,6 +7,7 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels +import dagger.hilt.android.AndroidEntryPoint import androidx.compose.animation.Crossfade import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -47,6 +48,7 @@ import java.io.File * Activity for managing Wi-Fi P2P hotspot for offline APK sharing. * Pure Compose implementation, no fragments. */ +@AndroidEntryPoint class HotspotActivity : ComponentActivity() { companion object { diff --git a/app/src/main/java/com/bitchat/android/hotspot/HotspotViewModel.kt b/app/src/main/java/com/bitchat/android/hotspot/HotspotViewModel.kt index 73f9faa4..d012ccbb 100644 --- a/app/src/main/java/com/bitchat/android/hotspot/HotspotViewModel.kt +++ b/app/src/main/java/com/bitchat/android/hotspot/HotspotViewModel.kt @@ -7,6 +7,8 @@ import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.bitchat.android.wifiaware.WifiAwareController +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -16,7 +18,10 @@ import java.io.File /** * ViewModel for managing hotspot state and lifecycle. */ -class HotspotViewModel(application: Application) : AndroidViewModel(application) { +@HiltViewModel +class HotspotViewModel @Inject constructor( + application: Application +) : AndroidViewModel(application) { companion object { private const val TAG = "HotspotViewModel" diff --git a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt index 54ab0318..0fa6bd72 100644 --- a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt @@ -63,8 +63,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.bitchat.android.ui.theme.BitchatFontFamily import com.bitchat.android.R import com.bitchat.android.core.ui.component.button.CloseButton @@ -617,7 +617,7 @@ fun AboutSheet( ) // === Prepare App for Sharing Section === - val apkViewModel: ApkDownloadViewModel = viewModel() + val apkViewModel: ApkDownloadViewModel = hiltViewModel() val apkUiState by apkViewModel.state.collectAsStateWithLifecycle() val apkStatus = apkUiState.apkStatus val releaseStatus = apkUiState.releaseStatus diff --git a/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt index 4d0c2d5b..dcc6d4af 100644 --- a/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt @@ -15,6 +15,8 @@ import com.bitchat.android.util.LatestReleaseProvider import com.bitchat.android.util.ShareableApkVariant import com.bitchat.android.util.UniversalApkManager import com.bitchat.android.util.WorkManagerApkDownloader +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel @@ -151,6 +153,7 @@ sealed class ApkUiEffect { * ViewModel for APK download/status/share logic following MVI pattern. * UI sends [ApkUiEvent], observes [ApkUiState], and collects [ApkUiEffect]. */ +@HiltViewModel class ApkDownloadViewModel internal constructor( application: Application, private val apkManager: UniversalApkManager, @@ -158,6 +161,11 @@ class ApkDownloadViewModel internal constructor( private val latestReleaseProvider: LatestReleaseProvider ) : AndroidViewModel(application) { + // Hilt injects through this constructor rather than the primary one: + // UniversalApkManager's collaborators are internal, and routing internal + // Kotlin types through Dagger's generated Java buys nothing here. Tests + // keep using the primary constructor to substitute fakes. + @Inject constructor(application: Application) : this( application = application, apkManager = UniversalApkManager(application), diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 76e784bc..ed1b4ec3 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -7,6 +7,8 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.bitchat.android.favorites.FavoritesChangeListener import com.bitchat.android.favorites.FavoritesPersistenceService +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -53,7 +55,8 @@ private data class ConversationLiveIdentityState( * Refactored ChatViewModel - Main coordinator for bitchat functionality * Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility */ -class ChatViewModel( +@HiltViewModel +class ChatViewModel @Inject constructor( application: Application, initialMeshService: BluetoothMeshService, initialUnifiedMeshService: MeshService From 480dc3833319f07e6a0a1d1926b2709020a4ee17 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Sat, 15 Aug 2026 01:53:06 +0300 Subject: [PATCH 07/14] style: group the new dagger imports with their neighbours The Hilt annotations landed mid-block in two files, splitting the androidx and kotlinx groups those files otherwise keep. No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NaZivRfLu7FdKjzVwwRjcc (cherry picked from commit 12b4b2aba288ec8875188d81cd60831764ce51f5) --- app/src/main/java/com/bitchat/android/MainViewModel.kt | 2 +- .../main/java/com/bitchat/android/hotspot/HotspotActivity.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/MainViewModel.kt b/app/src/main/java/com/bitchat/android/MainViewModel.kt index bd55ce02..aa30b6bd 100644 --- a/app/src/main/java/com/bitchat/android/MainViewModel.kt +++ b/app/src/main/java/com/bitchat/android/MainViewModel.kt @@ -5,9 +5,9 @@ import com.bitchat.android.onboarding.BluetoothStatus import com.bitchat.android.onboarding.LocationStatus import com.bitchat.android.onboarding.OnboardingState import com.bitchat.android.onboarding.BatteryOptimizationStatus +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject diff --git a/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt b/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt index 8f894f2d..41b6eb80 100644 --- a/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt +++ b/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt @@ -7,7 +7,6 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels -import dagger.hilt.android.AndroidEntryPoint import androidx.compose.animation.Crossfade import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -40,6 +39,7 @@ import com.bitchat.android.R import com.bitchat.android.ui.theme.BitchatFontFamily import com.bitchat.android.ui.theme.BitchatTheme import com.bitchat.android.util.UniversalApkManager +import dagger.hilt.android.AndroidEntryPoint import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState import java.io.File From c7d551e2aff4a77894b15678d537523e6f527018 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:10:27 +0300 Subject: [PATCH 08/14] feat(nav): put onboarding and chat behind a Navigation 3 back stack Replaces the when (onboardingState) in MainActivity and the ad-hoc back handling with a Navigation 3 back stack. Navigator, AppNavigator, the EntryProviderInstaller multibinding and the NavDisplay host live in :app for now; they move to a :core:navigation module once there is a second module to justify one. Onboarding is one destination, not eight. Its steps are driven by permission results and adapter state changes rather than by the user navigating, and going back a step has never been supported. Per-step routes would invent a history that does not exist, so the state machine stays and only the crossing into chat is a navigation event. That crossing uses resetTo, so Back cannot re-enter onboarding. Removes the OnBackPressedCallback that was constructed and registered inside a composable body, adding a callback on every recomposition of that branch. NavDisplay owns back now; it drives predictive back through androidx.navigationevent, and a second handler over the same gesture can detach the navigationevent input mid-gesture, so there must not be one. Chat still manages its overlays with booleans, so back consults ChatViewModel first through interceptBack. That parameter exists only until those overlays become routes. The back stack is seeded before setContent rather than from a LaunchedEffect: NavDisplay rejects an empty back stack and an effect does not run until after the first composition. Verified on a Pixel 9a: launch renders without the empty-backstack crash, chat is reached, Back exits to the launcher instead of re-entering onboarding, and relaunch is clean. Plus :app:assembleDebug and 591 unit tests. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26 --- app/build.gradle.kts | 4 + app/gradle.lockfile | 18 ++++- .../java/com/bitchat/android/MainActivity.kt | 72 +++++++++++++++--- .../android/navigation/AppNavigator.kt | 76 +++++++++++++++++++ .../bitchat/android/navigation/AppRoutes.kt | 35 +++++++++ .../android/navigation/BitchatNavDisplay.kt | 44 +++++++++++ .../navigation/EntryProviderInstaller.kt | 12 +++ .../android/navigation/NavigationModule.kt | 18 +++++ .../bitchat/android/navigation/Navigator.kt | 27 +++++++ gradle/libs.versions.toml | 11 +++ gradle/verification-metadata.xml | 65 ++++++++++++++++ 11 files changed, 366 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/navigation/AppNavigator.kt create mode 100644 app/src/main/java/com/bitchat/android/navigation/AppRoutes.kt create mode 100644 app/src/main/java/com/bitchat/android/navigation/BitchatNavDisplay.kt create mode 100644 app/src/main/java/com/bitchat/android/navigation/EntryProviderInstaller.kt create mode 100644 app/src/main/java/com/bitchat/android/navigation/NavigationModule.kt create mode 100644 app/src/main/java/com/bitchat/android/navigation/Navigator.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 939c1b33..81649153 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -198,6 +198,10 @@ dependencies { implementation(libs.androidx.exifinterface) // Testing + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.lifecycle.viewmodel.navigation3) + testImplementation(libs.bundles.testing) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.bundles.compose.testing) diff --git a/app/gradle.lockfile b/app/gradle.lockfile index 21a4ec95..62b09af7 100644 --- a/app/gradle.lockfile +++ b/app/gradle.lockfile @@ -116,21 +116,31 @@ androidx.lifecycle:lifecycle-viewmodel-android:2.11.0=debugAndroidTestCompileCla androidx.lifecycle:lifecycle-viewmodel-compose-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-navigation3-android:2.11.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-navigation3:2.11.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel-savedstate:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation3:navigation3-runtime-android:1.1.6=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath +androidx.navigation3:navigation3-runtime:1.1.6=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath +androidx.navigation3:navigation3-ui-android:1.1.6=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath +androidx.navigation3:navigation3-ui:1.1.6=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath androidx.navigation:navigation-common-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.navigation:navigation-common:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.navigation:navigation-compose-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.navigation:navigation-compose:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.navigation:navigation-runtime-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.navigation:navigation-runtime:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent-compose-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent-compose:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent-android:1.1.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath +androidx.navigationevent:navigationevent-compose-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent-compose-android:1.1.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath +androidx.navigationevent:navigationevent-compose:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent-compose:1.1.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath +androidx.navigationevent:navigationevent:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent:1.1.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath androidx.print:print:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.profileinstaller:profileinstaller:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.resourceinspection:resourceinspection-annotation:1.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 41d95a1e..280e7d5a 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -44,8 +44,15 @@ import com.bitchat.android.ui.OrientationAwareActivity import com.bitchat.android.ui.theme.BitchatTheme import com.bitchat.android.wifiaware.WifiAwareController import com.bitchat.android.nostr.PoWPreferenceManager +import com.bitchat.android.navigation.AppNavigator +import com.bitchat.android.navigation.BitchatNavDisplay +import com.bitchat.android.navigation.ChatRoute +import com.bitchat.android.navigation.EntryProviderInstaller +import com.bitchat.android.navigation.OnboardingRoute +import com.bitchat.android.navigation.rootRouteFor import com.bitchat.android.services.VerificationService import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -65,6 +72,11 @@ class MainActivity : OrientationAwareActivity() { private var pendingMeshForegroundServiceStart = false private val chatViewModel: ChatViewModel by viewModels() + // Held by ActivityRetainedComponent, so the back stack outlives configuration + // changes without being rebuilt here. + @Inject + lateinit var navigator: AppNavigator + private val forceFinishReceiver = object : android.content.BroadcastReceiver() { override fun onReceive(context: android.content.Context, intent: android.content.Intent) { if (intent.action == com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH) { @@ -148,16 +160,57 @@ class MainActivity : OrientationAwareActivity() { onOnboardingFailed = ::handleOnboardingFailed ) + // Seed the stack before the first composition. NavDisplay rejects an empty + // back stack, and a LaunchedEffect would not run until after it has already + // composed once. + navigator.setRootIfEmpty(rootRouteFor(mainViewModel.onboardingState.value)) + setContent { BitchatTheme { Scaffold( modifier = Modifier.fillMaxSize(), containerColor = MaterialTheme.colorScheme.background ) { innerPadding -> - OnboardingFlowScreen(modifier = Modifier + val contentModifier = Modifier .fillMaxSize() .padding(innerPadding) + val onboardingState by mainViewModel.onboardingState.collectAsState() + val root = rootRouteFor(onboardingState) + + // Keyed on root, so this fires only when the app crosses between + // onboarding and chat — not on every step within onboarding. + // resetTo rather than goTo: onboarding must not be reachable with + // Back once the app is in. + LaunchedEffect(root) { + if (navigator.backStack.lastOrNull() != root) { + navigator.resetTo(root) + } + } + + val entries: EntryProviderInstaller = { + entry { OnboardingFlowScreen(contentModifier) } + entry { ChatScreen(viewModel = chatViewModel) } + } + + BitchatNavDisplay( + navigator = navigator, + entryInstallers = setOf(entries), + onExit = { finish() }, + modifier = contentModifier, ) + + // Chat still drives its overlays with booleans rather than routes, + // and NavDisplay enables its own handler only while something sits + // beneath the current scene, which at the root destination is never. + // So the press has to be claimed here. Composed after NavDisplay: + // among enabled handlers the last one composed wins, so overlays + // unwind before routes pop. Removed once the overlays become routes. + val canHandleBack by chatViewModel.canHandleBack.collectAsState() + BackHandler( + enabled = navigator.backStack.lastOrNull() == ChatRoute && canHandleBack + ) { + chatViewModel.handleBackPressed() + } } } } @@ -306,17 +359,12 @@ class MainActivity : OrientationAwareActivity() { ) } - OnboardingState.CHECKING, OnboardingState.INITIALIZING, OnboardingState.COMPLETE -> { - val canHandleBack by chatViewModel.canHandleBack.collectAsState() - // Disabled rather than always-on so predictive back can preview - // the exit instead of the app claiming every gesture. - BackHandler(enabled = canHandleBack) { - // enabled trails the state by a dispatch and a recomposition. - if (!chatViewModel.handleBackPressed()) finish() - } - ChatScreen(viewModel = chatViewModel) - } - + // CHECKING, INITIALIZING and COMPLETE are handled by ChatRoute, so this + // composable is only ever shown for the onboarding steps themselves. + OnboardingState.CHECKING, + OnboardingState.INITIALIZING, + OnboardingState.COMPLETE -> Unit + OnboardingState.ERROR -> { InitializationErrorScreen( modifier = modifier, diff --git a/app/src/main/java/com/bitchat/android/navigation/AppNavigator.kt b/app/src/main/java/com/bitchat/android/navigation/AppNavigator.kt new file mode 100644 index 00000000..9ce3158c --- /dev/null +++ b/app/src/main/java/com/bitchat/android/navigation/AppNavigator.kt @@ -0,0 +1,76 @@ +package com.bitchat.android.navigation + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.navigation3.runtime.NavKey +import dagger.hilt.android.scopes.ActivityRetainedScoped +import javax.inject.Inject + +/** + * Single-stack navigator backed by a [SnapshotStateList] that NavDisplay observes. + * + * Scoped to [dagger.hilt.android.components.ActivityRetainedComponent], which is + * what makes the back stack survive configuration changes — there is no + * rememberNavBackStack here, and so no requirement that keys be serializable. + * + * The stack is *not* restored across process death. Nothing needs that yet: + * onboarding re-derives its state on launch. When it is needed, the move is + * kotlinx-serialization plus rememberNavBackStack, which is why keys are kept as + * plain data objects that would be trivial to annotate. + * + * Deliberately single-stack. The multi-back-stack pattern exists to serve bottom + * navigation; this app has no tabs, so it would be a map that only ever holds + * one key. + */ +@Stable +@ActivityRetainedScoped +class AppNavigator @Inject constructor() : Navigator { + + val backStack: SnapshotStateList = mutableStateListOf() + + /** + * Seeds the stack with [root] the first time only. + * + * Idempotent by design: an Activity recreated after a configuration change + * re-runs its setup, and the retained scope still holds the real history. + * Seeding again would discard it. + */ + fun setRootIfEmpty(root: NavKey) { + if (backStack.isEmpty()) backStack.add(root) + } + + override fun goTo(dest: NavKey) { + // A same-frame double tap pushes the same key twice. Dedup at the source + // rather than guarding every call site. + if (backStack.lastOrNull() != dest) backStack.add(dest) + } + + override fun goBack(): Boolean { + if (backStack.size <= 1) return false + backStack.removeAt(backStack.lastIndex) + return true + } + + override fun popTo(route: NavKey, inclusive: Boolean): Boolean { + val index = backStack.indexOfLast { it == route } + if (index < 0) return false + val target = if (inclusive) index else index + 1 + // Already at the requested route with inclusive = false pops nothing and + // still succeeded. + while (backStack.size > target) { + backStack.removeAt(backStack.lastIndex) + } + return true + } + + override fun replaceCurrent(dest: NavKey) { + if (backStack.isNotEmpty()) backStack.removeAt(backStack.lastIndex) + backStack.add(dest) + } + + override fun resetTo(dest: NavKey) { + backStack.clear() + backStack.add(dest) + } +} diff --git a/app/src/main/java/com/bitchat/android/navigation/AppRoutes.kt b/app/src/main/java/com/bitchat/android/navigation/AppRoutes.kt new file mode 100644 index 00000000..0eae3b97 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/navigation/AppRoutes.kt @@ -0,0 +1,35 @@ +package com.bitchat.android.navigation + +import androidx.navigation3.runtime.NavKey +import com.bitchat.android.onboarding.OnboardingState + +/** + * Top-level destinations. + * + * Onboarding is one destination, not eight. Its steps are driven by permission + * results and adapter state changes rather than by the user navigating, and the + * app has never supported going back from one step to the previous one. Giving + * each step its own entry would invent a history that does not exist. + * + * These live in :app because both destinations still need collaborators the + * Activity owns. They move to :feature::api as those features become + * modules. + */ +data object OnboardingRoute : NavKey + +data object ChatRoute : NavKey + +/** + * The destination that should be at the root for a given onboarding state. + * + * CHECKING and INITIALIZING map to chat, matching the behaviour this replaced: + * the app shows the chat screen while it verifies its own readiness rather than + * flashing an onboarding step. + */ +fun rootRouteFor(state: OnboardingState): NavKey = when (state) { + OnboardingState.CHECKING, + OnboardingState.INITIALIZING, + OnboardingState.COMPLETE -> ChatRoute + + else -> OnboardingRoute +} diff --git a/app/src/main/java/com/bitchat/android/navigation/BitchatNavDisplay.kt b/app/src/main/java/com/bitchat/android/navigation/BitchatNavDisplay.kt new file mode 100644 index 00000000..c0009527 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/navigation/BitchatNavDisplay.kt @@ -0,0 +1,44 @@ +package com.bitchat.android.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.ui.NavDisplay + +/** + * Hosts every feature's destinations. + * + * [entryInstallers] arrives as a Hilt multibinding, so adding a destination + * means adding an @IntoSet provider in the owning feature — this function never + * changes. + * + * This handles Back only between destinations, and only while there is one to + * go back to: NavDisplay enables its handler on scene.previousEntries being + * non-empty, so at the root destination it takes no press at all. A screen with + * state the back stack does not model has to claim Back itself, with a handler + * registered into the same dispatcher — androidx.activity.compose's BackHandler + * is, since activity-compose prefers the NavigationEventDispatcher when one is + * present. Among enabled handlers the last one composed wins. + */ +@Composable +fun BitchatNavDisplay( + navigator: AppNavigator, + entryInstallers: Set, + onExit: () -> Unit, + modifier: Modifier = Modifier, +) { + NavDisplay( + backStack = navigator.backStack, + modifier = modifier, + onBack = { if (!navigator.goBack()) onExit() }, + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator(), + ), + entryProvider = entryProvider { + entryInstallers.forEach { install -> install() } + }, + ) +} diff --git a/app/src/main/java/com/bitchat/android/navigation/EntryProviderInstaller.kt b/app/src/main/java/com/bitchat/android/navigation/EntryProviderInstaller.kt new file mode 100644 index 00000000..18d86803 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/navigation/EntryProviderInstaller.kt @@ -0,0 +1,12 @@ +package com.bitchat.android.navigation + +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey + +/** + * One feature's contribution to the navigation graph. + * + * Features provide these into a Set via Hilt multibindings, so the host + * assembles every destination without importing any feature's internals. + */ +typealias EntryProviderInstaller = EntryProviderScope.() -> Unit diff --git a/app/src/main/java/com/bitchat/android/navigation/NavigationModule.kt b/app/src/main/java/com/bitchat/android/navigation/NavigationModule.kt new file mode 100644 index 00000000..39895808 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/navigation/NavigationModule.kt @@ -0,0 +1,18 @@ +package com.bitchat.android.navigation + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent + +@Module +@InstallIn(ActivityRetainedComponent::class) +abstract class NavigationModule { + + /** + * Features inject [Navigator]; only the host injects [AppNavigator] itself, + * since reading or seeding the back stack is the host's job. + */ + @Binds + abstract fun bindNavigator(impl: AppNavigator): Navigator +} diff --git a/app/src/main/java/com/bitchat/android/navigation/Navigator.kt b/app/src/main/java/com/bitchat/android/navigation/Navigator.kt new file mode 100644 index 00000000..1c66b2bb --- /dev/null +++ b/app/src/main/java/com/bitchat/android/navigation/Navigator.kt @@ -0,0 +1,27 @@ +package com.bitchat.android.navigation + +import androidx.navigation3.runtime.NavKey + +/** + * The navigation surface features are allowed to depend on. + * + * Features call this rather than touching the back stack directly, so the + * guards below live in one place instead of at every call site. + */ +interface Navigator { + + /** Push [dest], unless it is already on top. */ + fun goTo(dest: NavKey) + + /** Pop one entry. Returns false when the stack cannot go back any further. */ + fun goBack(): Boolean + + /** Pop back to [route]. Returns false when [route] is not on the stack. */ + fun popTo(route: NavKey, inclusive: Boolean = false): Boolean + + /** Swap the current top for [dest], so Back skips the screen being replaced. */ + fun replaceCurrent(dest: NavKey) + + /** Clear the stack and start again at [dest]. */ + fun resetTo(dest: NavKey) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92034c89..1034a2f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,9 @@ compose-icons-extended = "1.7.8" # Navigation navigation-compose = "2.9.8" +# Latest stable line; the 1.2.0 branch is still alpha. 1.1.6 floors Compose at +# 1.10.0, below the 1.11.4 the BOM resolves, so it does not drag Compose forward. +navigation3 = "1.1.6" # Dependency injection hilt = "2.60.1" @@ -83,6 +86,11 @@ mockito-core = "5.23.0" roboelectric = "4.15" # 4.16+ drops the AndroidKeyStore shim; breaks EncryptionServiceTest [libraries] +# Gradle plugin artifacts, resolved only by build-logic convention plugins +android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } +kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } + # AndroidX Core androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime" } @@ -104,6 +112,9 @@ androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle- # Navigation androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" } +androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycle-runtime" } # Dependency injection hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8685436f..9066a436 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -5465,5 +5465,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From eb8c2e744623c6502deccd443870b4de9d6df403 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:32:12 +0300 Subject: [PATCH 09/14] style: drop the duplicated and speculative navigation commentary The reason interceptBack exists was written twice, at the call site and where the parameter is defined. Kept the one next to the definition. AppRoutes named :feature::api as the eventual home for the route types. Those modules do not exist and may not be called that; a comment that describes a plan rather than the code ages into a wrong one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26 --- app/src/main/java/com/bitchat/android/navigation/AppRoutes.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/navigation/AppRoutes.kt b/app/src/main/java/com/bitchat/android/navigation/AppRoutes.kt index 0eae3b97..fcfd2342 100644 --- a/app/src/main/java/com/bitchat/android/navigation/AppRoutes.kt +++ b/app/src/main/java/com/bitchat/android/navigation/AppRoutes.kt @@ -10,10 +10,6 @@ import com.bitchat.android.onboarding.OnboardingState * results and adapter state changes rather than by the user navigating, and the * app has never supported going back from one step to the previous one. Giving * each step its own entry would invent a history that does not exist. - * - * These live in :app because both destinations still need collaborators the - * Activity owns. They move to :feature::api as those features become - * modules. */ data object OnboardingRoute : NavKey From c56dfa826c990f65321bf5b4cf631ae7bb34c74d Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:45:42 +0300 Subject: [PATCH 10/14] build: lock Navigation 3 for release and trust guava's parent pom The nav3 entries were written only for the debug configurations, because the locks were refreshed with debug tasks. The root build sets LockMode.STRICT for every configuration, so resolving releaseCompileClasspath or releaseRuntimeClasspath rejected them and no release could be built. Refreshed following docs/reproducible-builds.md: the debug, lint and IDE copy-lock pass, then bundleRelease and assembleRelease in separate invocations because the split APK and AAB intermediates cannot coexist. Also adds com.google.guava:guava-parent:33.6.0-jre. Its pom is resolved through the KSP classpath that Hilt introduces, and a warm local cache never re-fetches it, so it only surfaced on CI. The checksum agrees across the local cache, a fresh Maven Central download, and the .sha256 Central publishes alongside it. One component added, none removed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26 --- app/gradle.lockfile | 24 +++--- gradle/verification-metadata.xml | 138 ++++++++++++++++--------------- 2 files changed, 83 insertions(+), 79 deletions(-) diff --git a/app/gradle.lockfile b/app/gradle.lockfile index 62b09af7..bc56f3ff 100644 --- a/app/gradle.lockfile +++ b/app/gradle.lockfile @@ -116,31 +116,27 @@ androidx.lifecycle:lifecycle-viewmodel-android:2.11.0=debugAndroidTestCompileCla androidx.lifecycle:lifecycle-viewmodel-compose-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-viewmodel-navigation3-android:2.11.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.lifecycle:lifecycle-viewmodel-navigation3:2.11.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-navigation3-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-navigation3:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel-savedstate:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigation3:navigation3-runtime-android:1.1.6=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.navigation3:navigation3-runtime:1.1.6=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.navigation3:navigation3-ui-android:1.1.6=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.navigation3:navigation3-ui:1.1.6=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath +androidx.navigation3:navigation3-runtime-android:1.1.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation3:navigation3-runtime:1.1.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation3:navigation3-ui-android:1.1.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation3:navigation3-ui:1.1.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.navigation:navigation-common-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.navigation:navigation-common:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.navigation:navigation-compose-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.navigation:navigation-compose:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.navigation:navigation-runtime-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.navigation:navigation-runtime:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent-android:1.1.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.navigationevent:navigationevent-compose-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent-compose-android:1.1.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.navigationevent:navigationevent-compose:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent-compose:1.1.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.navigationevent:navigationevent:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent:1.1.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath +androidx.navigationevent:navigationevent-android:1.1.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent-compose-android:1.1.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent-compose:1.1.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent:1.1.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.print:print:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.profileinstaller:profileinstaller:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.resourceinspection:resourceinspection-annotation:1.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9066a436..f5914c1d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1489,6 +1489,19 @@ + + + + + + + + + + + + + @@ -1557,11 +1570,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1570,11 +1614,24 @@ + + + + + + + + + + + + + @@ -1583,6 +1640,14 @@ + + + + + + + + @@ -3247,6 +3312,11 @@ + + + + + @@ -5178,6 +5248,9 @@ + + + @@ -5465,70 +5538,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From c0a8a3c6dfb649002e62d20b6ed545822104de6e Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:23:25 +0300 Subject: [PATCH 11/14] fix(nav): apply the scaffold insets at one layer, and trust the coroutines boms contentModifier already carried the Scaffold's system-bar padding and was passed to both NavDisplay and the onboarding entry, so onboarding padded itself twice while ChatScreen, which applies its own status, navigation and IME insets, gained an extra outer band. Only onboarding takes the scaffold padding now, matching what the when branch did before this PR, where ChatScreen was called with no modifier at all. The verification metadata refresh also adds three kotlinx-coroutines-bom poms. They resolve through detached configurations that KSP introduces, and a warm local cache never re-fetches them, so they failed only on a cold CI runner. Regenerated with --refresh-dependencies so the whole set was observed at once rather than one CI cycle at a time. Three components added, none removed, no version drift. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26 --- .../main/java/com/bitchat/android/MainActivity.kt | 8 +++++--- gradle/verification-metadata.xml | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 280e7d5a..143d2f3e 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -171,7 +171,9 @@ class MainActivity : OrientationAwareActivity() { modifier = Modifier.fillMaxSize(), containerColor = MaterialTheme.colorScheme.background ) { innerPadding -> - val contentModifier = Modifier + // Only onboarding takes the scaffold insets; ChatScreen applies + // its own status, navigation and IME padding. + val onboardingModifier = Modifier .fillMaxSize() .padding(innerPadding) val onboardingState by mainViewModel.onboardingState.collectAsState() @@ -188,7 +190,7 @@ class MainActivity : OrientationAwareActivity() { } val entries: EntryProviderInstaller = { - entry { OnboardingFlowScreen(contentModifier) } + entry { OnboardingFlowScreen(onboardingModifier) } entry { ChatScreen(viewModel = chatViewModel) } } @@ -196,7 +198,7 @@ class MainActivity : OrientationAwareActivity() { navigator = navigator, entryInstallers = setOf(entries), onExit = { finish() }, - modifier = contentModifier, + modifier = Modifier.fillMaxSize(), ) // Chat still drives its overlays with booleans rather than routes, diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index f5914c1d..0bd45df0 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -5092,11 +5092,21 @@ + + + + + + + + + + @@ -5107,6 +5117,11 @@ + + + + + From 1835281fc95fe375655d63f32cf4f5e943cd76d2 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:38:07 +0300 Subject: [PATCH 12/14] fix(nav): host the bluetooth receiver above the destinations The ACTION_STATE_CHANGED receiver was registered from a DisposableEffect inside OnboardingFlowScreen. That was harmless while the when branch kept that composable on screen for chat too, but chat is its own destination now, so reaching COMPLETE took the onboarding entry out of composition and unregistered the only receiver watching the adapter. Turning Bluetooth off from Quick Settings while sitting in chat then went unnoticed, and the onResume fallback does not fire without a lifecycle transition. Reproduced on a Pixel 9a. Disabling Bluetooth on the chat route logged nothing on this branch, against main: D BluetoothStatusManager: Bluetooth turned OFF W MainActivity: Bluetooth disabled or failed: User has turned off their Blue Hoisted next to NavDisplay, which lives for as long as the Activity's composition, so it no longer depends on which destination is showing. The callback also reads onboardingState from the ViewModel rather than the value captured when the effect ran, which the effect's keys never invalidated. Same two lines now log on the chat route, and 591 unit tests pass. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26 --- .../java/com/bitchat/android/MainActivity.kt | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 143d2f3e..d0047923 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -179,6 +179,32 @@ class MainActivity : OrientationAwareActivity() { val onboardingState by mainViewModel.onboardingState.collectAsState() val root = rootRouteFor(onboardingState) + // Hosted here rather than in the onboarding destination: turning + // Bluetooth off has to be noticed on the chat route too, and a + // destination-scoped receiver is unregistered when its entry leaves + // the back stack. + val context = LocalContext.current + DisposableEffect(context, bluetoothStatusManager) { + val receiver = bluetoothStatusManager.monitorBluetoothState( + context = context, + bluetoothStatusManager = bluetoothStatusManager, + onBluetoothStateChanged = { status -> + if (status == BluetoothStatus.ENABLED && + mainViewModel.onboardingState.value == OnboardingState.BLUETOOTH_CHECK + ) { + checkBluetoothAndProceed() + } + } + ) + onDispose { + try { + context.unregisterReceiver(receiver) + } catch (e: IllegalStateException) { + Log.w("BluetoothStatusUI", "Receiver was not registered") + } + } + } + // Keyed on root, so this fires only when the app crosses between // onboarding and chat — not on every step within onboarding. // resetTo rather than goTo: onboarding must not be reachable with @@ -246,7 +272,6 @@ class MainActivity : OrientationAwareActivity() { @Composable private fun OnboardingFlowScreen(modifier: Modifier = Modifier) { - val context = LocalContext.current val onboardingState by mainViewModel.onboardingState.collectAsState() val bluetoothStatus by mainViewModel.bluetoothStatus.collectAsState() val locationStatus by mainViewModel.locationStatus.collectAsState() @@ -256,27 +281,6 @@ class MainActivity : OrientationAwareActivity() { val isLocationLoading by mainViewModel.isLocationLoading.collectAsState() val isBatteryOptimizationLoading by mainViewModel.isBatteryOptimizationLoading.collectAsState() - DisposableEffect(context, bluetoothStatusManager) { - - val receiver = bluetoothStatusManager.monitorBluetoothState( - context = context, - bluetoothStatusManager = bluetoothStatusManager, - onBluetoothStateChanged = { status -> - if (status == BluetoothStatus.ENABLED && onboardingState == OnboardingState.BLUETOOTH_CHECK) { - checkBluetoothAndProceed() - } - } - ) - - onDispose { - try { - context.unregisterReceiver(receiver) - } catch (e: IllegalStateException) { - Log.w("BluetoothStatusUI", "Receiver was not registered") - } - } - } - when (onboardingState) { OnboardingState.PERMISSION_REQUESTING -> { InitializingScreen(modifier) From 664c2dac46ced09c7cd876e8442711aae0b75cda Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:42:27 +0300 Subject: [PATCH 13/14] refactor(nav): seed the back stack from the host, not from onCreate Three changes that all come from the back stack currently being one entry deep, which hides distinctions this PR is what makes matter. Seeding moved out of onCreate. It was there because NavDisplay rejects an empty back stack and effects do not run until after the first composition. Gating the host on the stack instead lets the effect that re-roots on every later crossing do the first one too, so there is one mechanism rather than two. It also stops the start route having to be readable synchronously: onCreate could only seed at all because onboardingState carries an eager CHECKING, and anything read from disk later would have had nothing to offer it. The gate has to test the stack rather than the route, since the route is non-null from the first composition and would compose the host with nothing to show. The crossing guard compares the first entry rather than the last. They are the same value at depth one, but the effect re-runs on every configuration change while the retained scope keeps the stack, so once a destination is pushed onto chat, the top would stop matching the root and every rotation would silently reset the stack. The Bluetooth receiver moves to onCreate and onDestroy. Composition and the Activity cover the same span here, so this is not a lifetime change; it is that a composition-scoped registration can be narrowed by moving the code, which is exactly how it came to be scoped to a destination and stopped firing on the chat route. setRootIfEmpty goes with the onCreate call it existed for. Verified on a Pixel 9a: launch without the empty-backstack crash, rotation both ways with the stack intact, and Bluetooth off then on still logged on the chat route. 591 unit tests. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26 --- .../java/com/bitchat/android/MainActivity.kt | 87 +++++++++---------- .../android/navigation/AppNavigator.kt | 11 --- 2 files changed, 43 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index d0047923..8b547f40 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.Lifecycle @@ -77,6 +76,11 @@ class MainActivity : OrientationAwareActivity() { @Inject lateinit var navigator: AppNavigator + // Watches the adapter for the whole Activity, not for as long as some + // composable happens to stay on screen. Scoping it to a destination is what + // let a Bluetooth switch-off go unnoticed once chat became its own route. + private var bluetoothStateReceiver: android.content.BroadcastReceiver? = null + private val forceFinishReceiver = object : android.content.BroadcastReceiver() { override fun onReceive(context: android.content.Context, intent: android.content.Intent) { if (intent.action == com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH) { @@ -160,11 +164,6 @@ class MainActivity : OrientationAwareActivity() { onOnboardingFailed = ::handleOnboardingFailed ) - // Seed the stack before the first composition. NavDisplay rejects an empty - // back stack, and a LaunchedEffect would not run until after it has already - // composed once. - navigator.setRootIfEmpty(rootRouteFor(mainViewModel.onboardingState.value)) - setContent { BitchatTheme { Scaffold( @@ -179,38 +178,14 @@ class MainActivity : OrientationAwareActivity() { val onboardingState by mainViewModel.onboardingState.collectAsState() val root = rootRouteFor(onboardingState) - // Hosted here rather than in the onboarding destination: turning - // Bluetooth off has to be noticed on the chat route too, and a - // destination-scoped receiver is unregistered when its entry leaves - // the back stack. - val context = LocalContext.current - DisposableEffect(context, bluetoothStatusManager) { - val receiver = bluetoothStatusManager.monitorBluetoothState( - context = context, - bluetoothStatusManager = bluetoothStatusManager, - onBluetoothStateChanged = { status -> - if (status == BluetoothStatus.ENABLED && - mainViewModel.onboardingState.value == OnboardingState.BLUETOOTH_CHECK - ) { - checkBluetoothAndProceed() - } - } - ) - onDispose { - try { - context.unregisterReceiver(receiver) - } catch (e: IllegalStateException) { - Log.w("BluetoothStatusUI", "Receiver was not registered") - } - } - } - - // Keyed on root, so this fires only when the app crosses between - // onboarding and chat — not on every step within onboarding. - // resetTo rather than goTo: onboarding must not be reachable with - // Back once the app is in. + // Seeds the stack on its first run and re-roots it on every + // later crossing between onboarding and chat. Keyed on root, so + // it stays quiet between onboarding steps. resetTo rather than + // goTo: onboarding must not be reachable with Back once the app + // is in. Compares the root of the stack, not its top, so pushing + // a destination onto chat does not read as a crossing. LaunchedEffect(root) { - if (navigator.backStack.lastOrNull() != root) { + if (navigator.backStack.firstOrNull() != root) { navigator.resetTo(root) } } @@ -220,19 +195,27 @@ class MainActivity : OrientationAwareActivity() { entry { ChatScreen(viewModel = chatViewModel) } } - BitchatNavDisplay( - navigator = navigator, - entryInstallers = setOf(entries), - onExit = { finish() }, - modifier = Modifier.fillMaxSize(), - ) + // NavDisplay rejects an empty back stack and the effect above does + // not run until after this composition, so the host waits a frame + // for it. Gate on the stack itself: root is non-null immediately, + // so gating on that would compose with nothing to show. + if (navigator.backStack.isNotEmpty()) { + BitchatNavDisplay( + navigator = navigator, + entryInstallers = setOf(entries), + onExit = { finish() }, + modifier = Modifier.fillMaxSize(), + ) + } // Chat still drives its overlays with booleans rather than routes, // and NavDisplay enables its own handler only while something sits // beneath the current scene, which at the root destination is never. // So the press has to be claimed here. Composed after NavDisplay: // among enabled handlers the last one composed wins, so overlays - // unwind before routes pop. Removed once the overlays become routes. + // unwind before routes pop. Called unconditionally and gated by + // enabled, because a conditional call would reorder composition. + // Removed once the overlays become routes. val canHandleBack by chatViewModel.canHandleBack.collectAsState() BackHandler( enabled = navigator.backStack.lastOrNull() == ChatRoute && canHandleBack @@ -243,6 +226,18 @@ class MainActivity : OrientationAwareActivity() { } } + bluetoothStateReceiver = bluetoothStatusManager.monitorBluetoothState( + context = this, + bluetoothStatusManager = bluetoothStatusManager, + onBluetoothStateChanged = { status -> + if (status == BluetoothStatus.ENABLED && + mainViewModel.onboardingState.value == OnboardingState.BLUETOOTH_CHECK + ) { + checkBluetoothAndProceed() + } + } + ) + // Collect state changes in a lifecycle-aware manner lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -902,6 +897,10 @@ class MainActivity : OrientationAwareActivity() { super.onDestroy() try { unregisterReceiver(forceFinishReceiver) } catch (_: Exception) { } + bluetoothStateReceiver?.let { + try { unregisterReceiver(it) } catch (_: IllegalArgumentException) { } + } + bluetoothStateReceiver = null // Cleanup location status manager try { diff --git a/app/src/main/java/com/bitchat/android/navigation/AppNavigator.kt b/app/src/main/java/com/bitchat/android/navigation/AppNavigator.kt index 9ce3158c..723b304a 100644 --- a/app/src/main/java/com/bitchat/android/navigation/AppNavigator.kt +++ b/app/src/main/java/com/bitchat/android/navigation/AppNavigator.kt @@ -29,17 +29,6 @@ class AppNavigator @Inject constructor() : Navigator { val backStack: SnapshotStateList = mutableStateListOf() - /** - * Seeds the stack with [root] the first time only. - * - * Idempotent by design: an Activity recreated after a configuration change - * re-runs its setup, and the retained scope still holds the real history. - * Seeding again would discard it. - */ - fun setRootIfEmpty(root: NavKey) { - if (backStack.isEmpty()) backStack.add(root) - } - override fun goTo(dest: NavKey) { // A same-frame double tap pushes the same key twice. Dedup at the source // rather than guarding every call site. From 3bdaf2ae5904a5dcd2058dfb85dd5659c07330a5 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:54:48 +0300 Subject: [PATCH 14/14] fix(ui): forward Back when the handler's enabled state is stale BackHandler's enabled flag trails the state it is derived from by a dispatch and a recomposition, so a second quick press after the last overlay closes still reaches this handler. handleBackPressed then finds nothing to unwind and returns false, and dropping that result swallowed the press instead of leaving the chat screen. The result is now forwarded: pop a route if one is beneath, and otherwise finish, which is what the press would have done had the handler already been disabled. This mirrors the guard the callback carried before the Navigation 3 host replaced it. Not covered by a test. The condition is a race between input dispatch and recomposition inside a composed hierarchy, and this project cannot host a Compose unit test yet: isIncludeAndroidResources fails 24 existing tests with "targetSdkVersion=37 > maxSdkVersion=35", and pinning Robolectric to sdk 35 fails 109. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LgmKvwg6yjo1cEmagppK9m --- app/src/main/java/com/bitchat/android/MainActivity.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 8b547f40..b0e56485 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -220,7 +220,11 @@ class MainActivity : OrientationAwareActivity() { BackHandler( enabled = navigator.backStack.lastOrNull() == ChatRoute && canHandleBack ) { - chatViewModel.handleBackPressed() + // enabled trails the state by a dispatch and a recomposition, so a + // second quick press can arrive with nothing left to unwind. Forward + // it rather than swallowing it: pop a route if there is one, and + // otherwise leave, which is what the press would have done anyway. + if (!chatViewModel.handleBackPressed() && !navigator.goBack()) finish() } } }