From 4ab588cdaaf0642d3ee4fd5e96935068ee383634 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:44:54 -0400 Subject: [PATCH] Migrate App Intro to Navigation 3 (#2983) --- app/build.gradle.kts | 1 + .../mesh/ui/intro/AppIntroductionScreen.kt | 132 ++++++++++-------- .../geeksville/mesh/ui/intro/IntroRoute.kt | 29 ---- gradle/libs.versions.toml | 6 + 4 files changed, 84 insertions(+), 84 deletions(-) delete mode 100644 app/src/main/java/com/geeksville/mesh/ui/intro/IntroRoute.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ff7d3730f..e8faff6df 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -232,6 +232,7 @@ dependencies { implementation(libs.bundles.adaptive) implementation(libs.bundles.lifecycle) implementation(libs.bundles.navigation) + implementation(libs.bundles.navigation3) implementation(libs.bundles.coroutines) implementation(libs.bundles.datastore) implementation(libs.bundles.room) diff --git a/app/src/main/java/com/geeksville/mesh/ui/intro/AppIntroductionScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/intro/AppIntroductionScreen.kt index 8fe04634b..b7eea81f8 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/intro/AppIntroductionScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/intro/AppIntroductionScreen.kt @@ -23,14 +23,17 @@ import android.os.Build import android.provider.Settings import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entry +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberPermissionState +import kotlinx.serialization.Serializable /** * Composable function for the main application introduction screen. This screen guides the user through initial setup @@ -43,7 +46,6 @@ import com.google.accompanist.permissions.rememberPermissionState @Composable fun AppIntroductionScreen(onDone: () -> Unit) { val context = LocalContext.current - val navController = rememberNavController() val notificationPermissionState: PermissionState? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -56,56 +58,76 @@ fun AppIntroductionScreen(onDone: () -> Unit) { listOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) val locationPermissionState = rememberMultiplePermissionsState(permissions = locationPermissions) - NavHost(navController = navController, startDestination = IntroRoute.Welcome.route) { - composable(IntroRoute.Welcome.route) { - WelcomeScreen(onGetStarted = { navController.navigate(IntroRoute.Notifications.route) }) - } - composable(IntroRoute.Notifications.route) { - val notificationsAlreadyGranted = notificationPermissionState?.status?.isGranted ?: true - NotificationsScreen( - showNextButton = notificationsAlreadyGranted, - onSkip = { navController.navigate(IntroRoute.Location.route) }, - onConfigure = { - if (notificationsAlreadyGranted) { - navController.navigate(IntroRoute.CriticalAlerts.route) - } else { - // For Android Tiramisu (API 33) and above, this requests POST_NOTIFICATIONS - // For lower versions, notificationPermissionState will be null, and this branch isn't taken. - notificationPermissionState.launchPermissionRequest() - } - }, - ) - } - composable(IntroRoute.CriticalAlerts.route) { - CriticalAlertsScreen( - onSkip = { navController.navigate(IntroRoute.Location.route) }, - onConfigure = { - // Intent to open the specific notification channel settings for "my_alerts" - // This allows the user to enable critical alerts if they were initially denied - // or to adjust settings for notifications that can bypass Do Not Disturb. - val intent = - Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { - putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) - putExtra(Settings.EXTRA_CHANNEL_ID, "my_alerts") + val backStack = rememberNavBackStack(Welcome) + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = + entryProvider { + entry { WelcomeScreen(onGetStarted = { backStack.add(Notifications) }) } + + entry { + val notificationsAlreadyGranted = notificationPermissionState?.status?.isGranted ?: true + NotificationsScreen( + showNextButton = notificationsAlreadyGranted, + onSkip = { + // Skip this screen and the Critical Alerts screen. Proceed to Location screen. + backStack.add(Location) + }, + onConfigure = { + if (notificationsAlreadyGranted) { + backStack.add(CriticalAlerts) + } else { + // For Android Tiramisu (API 33) and above, this requests POST_NOTIFICATIONS + // For lower versions, notificationPermissionState will be null, and this branch isn't + // taken. + notificationPermissionState?.launchPermissionRequest() } - context.startActivity(intent) - navController.navigate(IntroRoute.Location.route) - }, - ) - } - composable(IntroRoute.Location.route) { - val locationAlreadyGranted = locationPermissionState.allPermissionsGranted - LocationScreen( - showNextButton = locationAlreadyGranted, - onSkip = onDone, // Callback to signify completion of the intro flow - onConfigure = { - if (locationAlreadyGranted) { - onDone() // Permissions already granted, proceed to finish - } else { - locationPermissionState.launchMultiplePermissionRequest() - } - }, - ) - } - } + }, + ) + } + + entry { + CriticalAlertsScreen( + onSkip = { backStack.add(Location) }, + onConfigure = { + // Intent to open the specific notification channel settings for "my_alerts" + // This allows the user to enable critical alerts if they were initially denied + // or to adjust settings for notifications that can bypass Do Not Disturb. + val intent = + Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + putExtra(Settings.EXTRA_CHANNEL_ID, "my_alerts") + } + context.startActivity(intent) + backStack.add(Location) + }, + ) + } + + entry { + val locationAlreadyGranted = locationPermissionState.allPermissionsGranted + LocationScreen( + showNextButton = locationAlreadyGranted, + onSkip = onDone, // Callback to signify completion of the intro flow + onConfigure = { + if (locationAlreadyGranted) { + onDone() // Permissions already granted, proceed to finish + } else { + locationPermissionState.launchMultiplePermissionRequest() + } + }, + ) + } + }, + ) } + +@Serializable private data object Welcome : NavKey + +@Serializable private data object Notifications : NavKey + +@Serializable private data object CriticalAlerts : NavKey + +@Serializable private data object Location : NavKey diff --git a/app/src/main/java/com/geeksville/mesh/ui/intro/IntroRoute.kt b/app/src/main/java/com/geeksville/mesh/ui/intro/IntroRoute.kt deleted file mode 100644 index 4b2cc11ad..000000000 --- a/app/src/main/java/com/geeksville/mesh/ui/intro/IntroRoute.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.geeksville.mesh.ui.intro - -/** Sealed class defining type-safe navigation routes for the app introduction flow. */ -sealed class IntroRoute(val route: String) { - object Welcome : IntroRoute("welcome") - - object Notifications : IntroRoute("notifications") - - object Location : IntroRoute("location") - - object CriticalAlerts : IntroRoute("critical_alerts") -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f53839430..81ebbca7a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,6 +38,7 @@ material = "1.13.0" material3 = "1.5.0-alpha03" mgrs = "2.1.3" navigation = "2.9.3" +navigation3 = "1.0.0-alpha08" okhttp = "5.1.0" org-eclipse-paho-client-mqttv3 = "1.2.5" osmbonuspack = "6.9.0" @@ -129,6 +130,8 @@ material = { group = "com.google.android.material", name = "material", version.r mgrs = { group = "mil.nga", name = "mgrs", version.ref = "mgrs" } navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "navigation" } +navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "navigation3" } +navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "navigation3" } okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } org-eclipse-paho-client-mqttv3 = { group = "org.eclipse.paho", name = "org.eclipse.paho.client.mqttv3", version.ref = "org-eclipse-paho-client-mqttv3" } @@ -166,6 +169,9 @@ lifecycle = ["lifecycle-runtime-ktx", "lifecycle-livedata-ktx", "lifecycle-viewm # Navigation navigation = ["navigation-compose"] +# Navigation 3 +navigation3 = ["navigation3-runtime", "navigation3-ui"] + # Coroutines coroutines = ["kotlinx-coroutines-android", "kotlinx-coroutines-guava"]