Skip to content

Commit 506b327

Browse files
TimoPtrjpelgrom
andauthored
Always show LocationForSecureConnection when using HTTP while onboarding (#6223)
--------- Co-authored-by: Joris Pelgröm <jpelgrom@users.noreply.github.com>
1 parent bd90b6e commit 506b327

4 files changed

Lines changed: 33 additions & 97 deletions

File tree

app/src/main/kotlin/io/homeassistant/companion/android/onboarding/OnboardingNavigation.kt

Lines changed: 24 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package io.homeassistant.companion.android.onboarding
22

33
import android.app.Activity
4-
import android.content.pm.PackageManager
54
import android.net.Uri
65
import androidx.annotation.VisibleForTesting
7-
import androidx.compose.ui.util.fastAny
8-
import androidx.core.content.ContextCompat
96
import androidx.navigation.NavController
107
import androidx.navigation.NavGraphBuilder
118
import androidx.navigation.NavOptions
@@ -42,7 +39,6 @@ import io.homeassistant.companion.android.onboarding.wearmtls.navigation.wearMTL
4239
import io.homeassistant.companion.android.onboarding.welcome.navigation.WelcomeRoute
4340
import io.homeassistant.companion.android.onboarding.welcome.navigation.welcomeScreen
4441
import io.homeassistant.companion.android.util.canGoBack
45-
import io.homeassistant.companion.android.util.compose.locationPermissions
4642
import io.homeassistant.companion.android.util.compose.navigateToUri
4743
import kotlinx.serialization.Serializable
4844

@@ -84,23 +80,25 @@ data class WearOnboardingRoute(val wearName: String, val urlToOnboard: String? =
8480
* - Server accessibility (local vs public)
8581
* - App flavor (full with Google Play Services vs minimal FOSS)
8682
* - Connection security (HTTP vs HTTPS)
87-
* - Permission state (location access)
8883
*
8984
* ## Flow Overview
90-
* 1. Welcome screen (only shown if [skipWelcome] is false)
91-
* 2. Server discovery (only shown if [urlToOnboard] is empty)
92-
* 3. Connection
93-
* 3. Device naming and registration
94-
* 4. Location/security configuration (conditional)
95-
* 5. Home network configuration (if applicable)
85+
* 1. Welcome screen (skipped if [skipWelcome] is true)
86+
* 2. Server discovery (skipped if [urlToOnboard] is provided)
87+
* 3. Connection and authentication
88+
* 4. Device naming and registration
89+
* 5. Post-registration flow based on server configuration:
90+
* - Local-only servers: Local first screen → (full flavor only: Location sharing →) Secure connection (HTTP) or done (HTTPS)
91+
* - Public servers with full flavor: Location sharing → Secure connection (HTTP) or done (HTTPS)
92+
* - Public servers with minimal flavor: Secure connection (HTTP) or done (HTTPS)
93+
* 6. Home network configuration (if user opts for secure-only connection)
9694
*
9795
* @param navController Navigation controller for managing navigation actions
9896
* @param onShowSnackbar Callback to display snackbar messages to the user
9997
* @param onOnboardingDone Callback invoked when onboarding completes successfully
10098
* @param urlToOnboard Optional server URL to onboard directly, bypassing server discovery
10199
* @param hideExistingServers When true, hides already registered servers from discovery
102100
* @param skipWelcome When true, skips the welcome screen and goes directly to server discovery or connection
103-
* @param hasLocationTracking Whether location tracking is available (default to full flavor = true, minimal = false)
101+
* @param hasLocationTracking Whether location tracking is available (full flavor = true, minimal = false)
104102
*/
105103
internal fun NavGraphBuilder.onboarding(
106104
navController: NavController,
@@ -172,7 +170,7 @@ internal fun NavGraphBuilder.onboarding(
172170
navOptions = navOptions,
173171
)
174172
} else {
175-
navController.navigateForMinimalFlavor(
173+
navController.navigateToLocationForSecureConnectionConditionally(
176174
serverId = serverId,
177175
hasPlainTextAccess = hasPlainTextAccess,
178176
navOptions = navOptions,
@@ -187,12 +185,12 @@ internal fun NavGraphBuilder.onboarding(
187185
navController.navigateToUri(URL_GETTING_STARTED_DOCUMENTATION)
188186
},
189187
onGotoNextScreen = { serverId, hasPlainTextAccess ->
190-
navController.navigateForMinimalFlavor(
188+
navController.navigateToLocationForSecureConnectionConditionally(
191189
serverId = serverId,
192-
hasPlainTextAccess = hasPlainTextAccess,
193190
navOptions = navOptions {
194191
popUpTo<LocationSharingRoute> { inclusive = true }
195192
},
193+
hasPlainTextAccess = hasPlainTextAccess,
196194
onOnboardingDone = onOnboardingDone,
197195
)
198196
},
@@ -297,26 +295,13 @@ private fun NavGraphBuilder.commonScreens(navController: NavController, wearName
297295
)
298296
}
299297

300-
/**
301-
* Checks if location permissions need to be requested.
302-
*
303-
* @return `true` if any location permission is not granted, `false` if all are granted
304-
*/
305-
private fun NavController.shouldRequestLocationPermissions(): Boolean {
306-
return locationPermissions.fastAny {
307-
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_DENIED
308-
}
309-
}
310-
311298
/**
312299
* Navigates to the appropriate next screen after device registration based on server configuration.
313300
*
314-
* This function encapsulates the complex decision tree for post-registration navigation,
315-
* considering:
316-
* - Whether the server is publicly accessible
317-
* - Whether location tracking is available (full vs minimal flavor)
318-
* - Whether the connection uses plain text HTTP
319-
* - Whether location permissions are already granted
301+
* Navigation decision tree:
302+
* - Local-only server (!isPubliclyAccessible) → Local first screen
303+
* - Public server with location tracking (full flavor) → Location sharing screen
304+
* - Public server without location tracking (minimal flavor) → Secure connection (HTTP) or done (HTTPS)
320305
*
321306
* @param serverId The ID of the registered server
322307
* @param hasPlainTextAccess Whether the server connection uses HTTP (insecure)
@@ -346,8 +331,7 @@ private fun NavController.navigateAfterDeviceRegistration(
346331
hasPlainTextAccess = hasPlainTextAccess,
347332
navOptions = navOptions,
348333
)
349-
// Minimal flavor: handle location and security based on connection type
350-
else -> navigateForMinimalFlavor(
334+
else -> navigateToLocationForSecureConnectionConditionally(
351335
serverId = serverId,
352336
hasPlainTextAccess = hasPlainTextAccess,
353337
navOptions = navOptions,
@@ -357,14 +341,13 @@ private fun NavController.navigateAfterDeviceRegistration(
357341
}
358342

359343
/**
360-
* Navigates to the appropriate screen for minimal flavor after location-related setup.
344+
* Conditionally navigates to the secure connection screen based on connection security.
361345
*
362-
* For minimal flavor (without location tracking), the flow is:
363-
* - If HTTPS: onboarding is complete
364-
* - If HTTP and no location permission: ask for location to enable secure connection detection
365-
* - If HTTP and has location permission: configure home network
346+
* Navigation decision:
347+
* - HTTPS connection (!hasPlainTextAccess) → Onboarding complete
348+
* - HTTP connection → Navigate to secure connection screen to configure home network detection
366349
*/
367-
private fun NavController.navigateForMinimalFlavor(
350+
private fun NavController.navigateToLocationForSecureConnectionConditionally(
368351
serverId: Int,
369352
hasPlainTextAccess: Boolean,
370353
navOptions: NavOptions,
@@ -376,12 +359,7 @@ private fun NavController.navigateForMinimalFlavor(
376359
return
377360
}
378361

379-
// HTTP connection: need location for secure connection detection
380-
if (shouldRequestLocationPermissions()) {
381-
navigateToLocationForSecureConnection(serverId = serverId, navOptions = navOptions)
382-
} else {
383-
navigateToSetHomeNetworkRoute(serverId = serverId, navOptions = navOptions)
384-
}
362+
navigateToLocationForSecureConnection(serverId = serverId, navOptions = navOptions)
385363
}
386364

387365
/**

app/src/test/kotlin/io/homeassistant/companion/android/onboarding/localfirst/navigation/LocalFirstNavigationTest.kt

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import io.homeassistant.companion.android.common.R as commonR
1010
import io.homeassistant.companion.android.onboarding.BaseOnboardingNavigationTest
1111
import io.homeassistant.companion.android.onboarding.locationforsecureconnection.navigation.LocationForSecureConnectionRoute
1212
import io.homeassistant.companion.android.onboarding.locationsharing.navigation.LocationSharingRoute
13-
import io.homeassistant.companion.android.onboarding.sethomenetwork.navigation.SetHomeNetworkRoute
1413
import io.homeassistant.companion.android.onboarding.welcome.navigation.WelcomeRoute
1514
import io.homeassistant.companion.android.testing.unit.stringResource
1615
import junit.framework.TestCase.assertTrue
@@ -46,9 +45,8 @@ internal class LocalFirstNavigationTest : BaseOnboardingNavigationTest() {
4645
}
4746

4847
@Test
49-
fun `Given no location tracking from LocalFirst with HTTP and no permission when next clicked then show LocationForSecureConnection`() {
48+
fun `Given no location tracking from LocalFirst with HTTP when next clicked then show LocationForSecureConnection`() {
5049
testNavigation(hasLocationTracking = false) {
51-
mockCheckPermission(false)
5250
navController.navigateToLocalFirst(serverId = 42, hasPlainTextAccess = true)
5351
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<LocalFirstRoute>() == true)
5452

@@ -62,21 +60,6 @@ internal class LocalFirstNavigationTest : BaseOnboardingNavigationTest() {
6260
}
6361
}
6462

65-
@Test
66-
fun `Given no location tracking from LocalFirst with HTTP and has permission when next clicked then show SetHomeNetwork`() {
67-
testNavigation(hasLocationTracking = false) {
68-
mockCheckPermission(true)
69-
navController.navigateToLocalFirst(serverId = 42, hasPlainTextAccess = true)
70-
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<LocalFirstRoute>() == true)
71-
72-
onNodeWithText(stringResource(commonR.string.local_first_next))
73-
.performScrollTo()
74-
.performClick()
75-
76-
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<SetHomeNetworkRoute>() == true)
77-
}
78-
}
79-
8063
@Test
8164
fun `Given no location tracking from LocalFirst with HTTPS when next clicked then onboarding completes`() {
8265
testNavigation(hasLocationTracking = false) {

app/src/test/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/navigation/LocationSharingNavigationTest.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import dagger.hilt.android.testing.HiltTestApplication
99
import io.homeassistant.companion.android.common.R as commonR
1010
import io.homeassistant.companion.android.onboarding.BaseOnboardingNavigationTest
1111
import io.homeassistant.companion.android.onboarding.locationforsecureconnection.navigation.LocationForSecureConnectionRoute
12-
import io.homeassistant.companion.android.onboarding.sethomenetwork.navigation.SetHomeNetworkRoute
1312
import io.homeassistant.companion.android.onboarding.welcome.navigation.WelcomeRoute
1413
import io.homeassistant.companion.android.testing.unit.stringResource
1514
import junit.framework.TestCase.assertTrue
@@ -28,7 +27,7 @@ import org.robolectric.annotation.Config
2827
internal class LocationSharingNavigationTest : BaseOnboardingNavigationTest() {
2928

3029
@Test
31-
fun `Given LocationSharing when agreeing with plain text access to share then show SetHomeNetwork`() {
30+
fun `Given LocationSharing when agreeing with plain text access to share then show LocationForSecureConnection then goes back stop the app`() {
3231
testNavigation {
3332
navController.navigateToLocationSharing(42, true)
3433
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<LocationSharingRoute>() == true)
@@ -38,7 +37,12 @@ internal class LocationSharingNavigationTest : BaseOnboardingNavigationTest() {
3837
.performScrollTo()
3938
.performClick()
4039

41-
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<SetHomeNetworkRoute>() == true)
40+
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<LocationForSecureConnectionRoute>() == true)
41+
42+
composeTestRule.activity.onBackPressedDispatcher.onBackPressed()
43+
44+
// In the test scenario since we never opened NameYourDevice the stack still contains Welcome
45+
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<WelcomeRoute>() == true)
4246
}
4347
}
4448

app/src/test/kotlin/io/homeassistant/companion/android/onboarding/nameyourdevice/navigation/NameYourDeviceNavigationTest.kt

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ import io.homeassistant.companion.android.onboarding.locationforsecureconnection
1616
import io.homeassistant.companion.android.onboarding.locationsharing.navigation.LocationSharingRoute
1717
import io.homeassistant.companion.android.onboarding.nameyourdevice.NameYourDeviceNavigationEvent
1818
import io.homeassistant.companion.android.onboarding.nameyourdevice.NameYourDeviceViewModel
19-
import io.homeassistant.companion.android.onboarding.sethomenetwork.navigation.SetHomeNetworkRoute
2019
import io.homeassistant.companion.android.testing.unit.stringResource
21-
import io.mockk.coEvery
2220
import io.mockk.every
2321
import io.mockk.mockk
2422
import junit.framework.TestCase.assertTrue
@@ -150,7 +148,7 @@ internal class NameYourDeviceNavigationTest : BaseOnboardingNavigationTest() {
150148
}
151149

152150
@Test
153-
fun `Given no location tracking with HTTP public server and no location permission when device named then show LocationForSecureConnection`() {
151+
fun `Given no location tracking with HTTP public server when device named then show LocationForSecureConnection`() {
154152
every { nameYourDeviceViewModel.onSaveClick() } coAnswers {
155153
nameYourDeviceNavigationFlow.emit(
156154
NameYourDeviceNavigationEvent.DeviceNameSaved(
@@ -161,7 +159,6 @@ internal class NameYourDeviceNavigationTest : BaseOnboardingNavigationTest() {
161159
)
162160
}
163161
testNavigation(hasLocationTracking = false) {
164-
mockCheckPermission(false)
165162
navController.navigateToNameYourDevice("http://homeassistant.local", "code")
166163
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<NameYourDeviceRoute>() == true)
167164

@@ -176,30 +173,4 @@ internal class NameYourDeviceNavigationTest : BaseOnboardingNavigationTest() {
176173
)
177174
}
178175
}
179-
180-
@Test
181-
fun `Given no location tracking with HTTP public server and has location permission when device named then show SetHomeNetwork`() {
182-
coEvery { nameYourDeviceViewModel.onSaveClick() } coAnswers {
183-
nameYourDeviceNavigationFlow.emit(
184-
NameYourDeviceNavigationEvent.DeviceNameSaved(
185-
serverId = 42,
186-
hasPlainTextAccess = true,
187-
isPubliclyAccessible = true,
188-
),
189-
)
190-
}
191-
testNavigation(hasLocationTracking = false) {
192-
mockCheckPermission(true)
193-
navController.navigateToNameYourDevice("http://homeassistant.local", "code")
194-
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<NameYourDeviceRoute>() == true)
195-
196-
onNodeWithText(stringResource(commonR.string.name_your_device_save))
197-
.performScrollTo()
198-
.assertIsDisplayed()
199-
.assertIsEnabled()
200-
.performClick()
201-
202-
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<SetHomeNetworkRoute>() == true)
203-
}
204-
}
205176
}

0 commit comments

Comments
 (0)