Skip to content

Commit efcc9fa

Browse files
TimoPtrjpelgrom
andauthored
Catch open URI to avoid crashing the app (#6290)
--------- Co-authored-by: Joris Pelgröm <jpelgrom@users.noreply.github.com>
1 parent 6022cfa commit efcc9fa

File tree

7 files changed

+59
-17
lines changed

7 files changed

+59
-17
lines changed

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import io.homeassistant.companion.android.onboarding.welcome.navigation.WelcomeR
4040
import io.homeassistant.companion.android.onboarding.welcome.navigation.welcomeScreen
4141
import io.homeassistant.companion.android.util.canGoBack
4242
import io.homeassistant.companion.android.util.compose.navigateToUri
43+
import io.homeassistant.companion.android.util.compose.navigateToUriCatching
4344
import kotlinx.serialization.Serializable
4445

4546
@VisibleForTesting
@@ -134,7 +135,7 @@ internal fun NavGraphBuilder.onboarding(
134135
navController.navigateToUri(URL_GETTING_STARTED_DOCUMENTATION)
135136
},
136137
)
137-
commonScreens(navController = navController)
138+
commonScreens(navController = navController, onShowSnackbar = onShowSnackbar)
138139
nameYourDeviceScreen(
139140
onBackClick = navController::popBackStack,
140141
onDeviceNamed = { serverId, hasPlainTextAccess, isPubliclyAccessible ->
@@ -235,7 +236,11 @@ internal fun NavGraphBuilder.onboarding(
235236
* - Manual server entry: Direct URL input for server connection
236237
* - Connection: Authentication and server validation
237238
*/
238-
private fun NavGraphBuilder.commonScreens(navController: NavController, wearNameToOnboard: String? = null) {
239+
private fun NavGraphBuilder.commonScreens(
240+
navController: NavController,
241+
onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
242+
wearNameToOnboard: String? = null,
243+
) {
239244
serverDiscoveryScreen(
240245
onConnectClick = {
241246
navController.navigateToConnection(it.toString())
@@ -290,7 +295,7 @@ private fun NavGraphBuilder.commonScreens(navController: NavController, wearName
290295
},
291296
onBackClick = navController::popBackStack,
292297
onOpenExternalLink = {
293-
navController.navigateToUri(it.toString())
298+
navController.navigateToUriCatching(it.toString(), onShowSnackbar = onShowSnackbar)
294299
},
295300
)
296301
}
@@ -388,6 +393,7 @@ internal fun NavGraphBuilder.wearOnboarding(
388393
certUri: Uri?,
389394
certPassword: String?,
390395
) -> Unit,
396+
onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
391397
urlToOnboard: String?,
392398
wearNameToOnboard: String,
393399
) {
@@ -398,7 +404,11 @@ internal fun NavGraphBuilder.wearOnboarding(
398404
}
399405

400406
navigation<WearOnboardingRoute>(startDestination = startRoute) {
401-
commonScreens(navController = navController, wearNameToOnboard = wearNameToOnboard)
407+
commonScreens(
408+
navController = navController,
409+
onShowSnackbar = onShowSnackbar,
410+
wearNameToOnboard = wearNameToOnboard,
411+
)
402412
nameYourWearDeviceScreen(
403413
onBackClick = navController::popBackStack,
404414
onDeviceNamed = { deviceName, serverUrl, authCode, neededMTLS ->

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

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import androidx.compose.material3.Text
2727
import androidx.compose.runtime.Composable
2828
import androidx.compose.runtime.collectAsState
2929
import androidx.compose.runtime.getValue
30+
import androidx.compose.runtime.rememberCoroutineScope
3031
import androidx.compose.ui.Alignment
3132
import androidx.compose.ui.Modifier
3233
import androidx.compose.ui.graphics.vector.ImageVector
@@ -52,6 +53,7 @@ import io.homeassistant.companion.android.common.compose.theme.HAThemeForPreview
5253
import io.homeassistant.companion.android.common.compose.theme.MaxButtonWidth
5354
import io.homeassistant.companion.android.common.data.connectivity.ConnectivityCheckState
5455
import io.homeassistant.companion.android.util.compose.HAPreviews
56+
import kotlinx.coroutines.launch
5557

5658
private val MaxContentWidth = MaxButtonWidth
5759

@@ -66,7 +68,7 @@ private const val URL_DISCORD = "https://discord.com/channels/330944238910963714
6668
@Composable
6769
internal fun ConnectionErrorScreen(
6870
viewModel: ConnectionViewModel,
69-
onOpenExternalLink: (Uri) -> Unit,
71+
onOpenExternalLink: suspend (Uri) -> Unit,
7072
onCloseClick: () -> Unit,
7173
modifier: Modifier = Modifier,
7274
) {
@@ -91,7 +93,7 @@ internal fun ConnectionErrorScreen(
9193
error: ConnectionError?,
9294
connectivityCheckState: ConnectivityCheckState,
9395
onRetryConnectivityCheck: () -> Unit,
94-
onOpenExternalLink: (Uri) -> Unit,
96+
onOpenExternalLink: suspend (Uri) -> Unit,
9597
onCloseClick: () -> Unit,
9698
modifier: Modifier = Modifier,
9799
errorDetailsExpanded: Boolean = false,
@@ -123,7 +125,7 @@ internal fun ConnectionErrorScreen(
123125

124126
@Composable
125127
internal fun ConnectionErrorScreen(
126-
onOpenExternalLink: (Uri) -> Unit,
128+
onOpenExternalLink: suspend (Uri) -> Unit,
127129
icon: ImageVector,
128130
title: String,
129131
subtitle: String,
@@ -159,7 +161,7 @@ internal fun ConnectionErrorScreen(
159161

160162
@Composable
161163
private fun ConnectionErrorContent(
162-
onOpenExternalLink: (Uri) -> Unit,
164+
onOpenExternalLink: suspend (Uri) -> Unit,
163165
title: String,
164166
subtitle: String,
165167
errorDescription: String,
@@ -230,13 +232,14 @@ private fun ColumnScope.Header(icon: ImageVector, title: String, subtitle: Strin
230232
}
231233

232234
@Composable
233-
private fun UrlInfo(url: String?, onOpenExternalLink: (Uri) -> Unit) {
235+
private fun UrlInfo(url: String?, onOpenExternalLink: suspend (Uri) -> Unit) {
234236
url?.let { url ->
235237
HABanner(
236238
modifier = Modifier
237239
.width(MaxContentWidth)
238240
.testTag(URL_INFO_TAG),
239241
) {
242+
val coroutineContext = rememberCoroutineScope()
240243
val annotatedString = buildAnnotatedString {
241244
append(stringResource(commonR.string.connection_error_url_info))
242245
appendLine()
@@ -245,7 +248,9 @@ private fun UrlInfo(url: String?, onOpenExternalLink: (Uri) -> Unit) {
245248
url,
246249
styles = HATextStyle.Link,
247250
linkInteractionListener = {
248-
onOpenExternalLink(url.toUri())
251+
coroutineContext.launch {
252+
onOpenExternalLink(url.toUri())
253+
}
249254
},
250255
),
251256
) {
@@ -314,36 +319,45 @@ private fun ErrorDetails(
314319
}
315320

316321
@Composable
317-
private fun ColumnScope.GetMoreHelp(onOpenExternalLink: (Uri) -> Unit) {
322+
private fun ColumnScope.GetMoreHelp(onOpenExternalLink: suspend (Uri) -> Unit) {
323+
val coroutineScope = rememberCoroutineScope()
318324
Text(stringResource(commonR.string.connection_error_help), style = HATextStyle.Body)
319325

320326
Row {
321327
HAIconButton(
322328
icon = Icons.Outlined.Newspaper,
323329
contentDescription = stringResource(commonR.string.connection_error_documentation_content_description),
324330
onClick = {
325-
onOpenExternalLink(URL_DOCUMENTATION.toUri())
331+
coroutineScope.launch {
332+
onOpenExternalLink(URL_DOCUMENTATION.toUri())
333+
}
326334
},
327335
)
328336
HAIconButton(
329337
icon = Icons.Outlined.Forum,
330338
contentDescription = stringResource(commonR.string.connection_error_forum_content_description),
331339
onClick = {
332-
onOpenExternalLink(URL_COMMUNITY_FORUM.toUri())
340+
coroutineScope.launch {
341+
onOpenExternalLink(URL_COMMUNITY_FORUM.toUri())
342+
}
333343
},
334344
)
335345
HAIconButton(
336346
icon = ImageVector.vectorResource(R.drawable.github),
337347
contentDescription = stringResource(commonR.string.connection_error_github_content_description),
338348
onClick = {
339-
onOpenExternalLink(URL_GITHUB_ISSUES.toUri())
349+
coroutineScope.launch {
350+
onOpenExternalLink(URL_GITHUB_ISSUES.toUri())
351+
}
340352
},
341353
)
342354
HAIconButton(
343355
icon = ImageVector.vectorResource(R.drawable.discord),
344356
contentDescription = stringResource(commonR.string.connection_error_discord_content_description),
345357
onClick = {
346-
onOpenExternalLink(URL_DISCORD.toUri())
358+
coroutineScope.launch {
359+
onOpenExternalLink(URL_DISCORD.toUri())
360+
}
347361
},
348362
)
349363
}

app/src/main/kotlin/io/homeassistant/companion/android/onboarding/connection/navigation/ConnectionNavigation.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal fun NavController.navigateToConnection(url: String, navOptions: NavOpti
2525
internal fun NavGraphBuilder.connectionScreen(
2626
onAuthenticated: (url: String, authCode: String, requiredMTLS: Boolean) -> Unit,
2727
onBackClick: () -> Unit,
28-
onOpenExternalLink: (url: Uri) -> Unit,
28+
onOpenExternalLink: suspend (url: Uri) -> Unit,
2929
) {
3030
composable<ConnectionRoute> {
3131
val viewModel: ConnectionViewModel = hiltViewModel()
@@ -54,7 +54,7 @@ internal fun NavGraphBuilder.connectionScreen(
5454
internal fun HandleConnectionNavigationEvents(
5555
viewModel: ConnectionViewModel,
5656
onAuthenticated: (url: String, authCode: String, requiredMTLS: Boolean) -> Unit,
57-
onOpenExternalLink: (url: Uri) -> Unit,
57+
onOpenExternalLink: suspend (url: Uri) -> Unit,
5858
) {
5959
LaunchedEffect(viewModel) {
6060
viewModel.navigationEventsFlow.collect {

app/src/main/kotlin/io/homeassistant/companion/android/util/compose/HANavHost.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ internal fun HANavHost(
8888
)
8989
activity?.finish()
9090
},
91+
onShowSnackbar = onShowSnackbar,
9192
urlToOnboard = startDestination.urlToOnboard,
9293
wearNameToOnboard = startDestination.wearName,
9394
)

app/src/main/kotlin/io/homeassistant/companion/android/util/compose/NavControllerExt.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,22 @@ package io.homeassistant.companion.android.util.compose
22

33
import androidx.compose.ui.platform.AndroidUriHandler
44
import androidx.navigation.NavController
5+
import io.homeassistant.companion.android.common.R as commonR
6+
import timber.log.Timber
57

68
fun NavController.navigateToUri(uri: String) {
79
AndroidUriHandler(context).openUri(uri)
810
}
11+
12+
suspend fun NavController.navigateToUriCatching(
13+
uri: String,
14+
onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
15+
) {
16+
try {
17+
AndroidUriHandler(context).openUri(uri)
18+
} catch (e: IllegalArgumentException) {
19+
// Don't log e to not leak the URL in the log
20+
Timber.e("Failed to navigate to uri")
21+
onShowSnackbar(context.getString(commonR.string.fail_to_navigate_to_uri, uri), null)
22+
}
23+
}

app/src/test/kotlin/io/homeassistant/companion/android/onboarding/WearOnboardingNavigationTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ internal class WearOnboardingNavigationTest {
175175
this@WearOnboardingNavigationTest.certUri = certUri
176176
this@WearOnboardingNavigationTest.certPassword = certPassword
177177
},
178+
onShowSnackbar = { _, _ -> true },
178179
urlToOnboard = urlToOnboard,
179180
wearNameToOnboard = WEAR_NAME,
180181
)

common/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,4 +1435,5 @@
14351435
<string name="connection_check_error_not_home_assistant">Server is not Home Assistant</string>
14361436
<string name="entity_picker_no_entity_found">No entity found</string>
14371437
<string name="entity_picker_no_entity_found_for">No entity found for %s</string>
1438+
<string name="fail_to_navigate_to_uri">No app available to open %s</string>
14381439
</resources>

0 commit comments

Comments
 (0)