From 111950758524bc0c8b41f12f9d477ea36d44f162 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 9 Sep 2026 11:34:50 +0200 Subject: [PATCH] sync: a device with no browser no longer strands the flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both launches can fail, and the outer catch named only ActivityNotFoundException — so a SecurityException from a locked-down profile escaped the LaunchedEffect and took the app down. The failure was also ignored: onBrowserLaunched cleared openInBrowser regardless, so the user sat on "waiting for your browser" with a spinner and no browser for the rest of the twenty-minute window, on exactly the AOSP and GrapheneOS devices the Custom Tabs fallback exists for. A failed launch now says so, and the step offers the password path, which is the only way forward on such a device. --- .../agendula/ui/accounts/AddAccountMessage.kt | 3 +++ .../agendula/ui/accounts/AddAccountScreen.kt | 20 +++++++++++++------ .../ui/accounts/AddAccountViewModel.kt | 15 ++++++++++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountMessage.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountMessage.kt index 7d044ca..c1f950a 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountMessage.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountMessage.kt @@ -42,6 +42,9 @@ sealed interface AddAccountMessage { data object BrowserMaintenance : Problem data object BrowserFailed : Problem + /** No browser could be opened at all — AOSP, GrapheneOS, a locked-down profile. */ + data object BrowserUnavailable : Problem + /** * A home set on a host the credential is not scoped to. * diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountScreen.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountScreen.kt index 3db6cbd..c88d746 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountScreen.kt @@ -1,6 +1,5 @@ package de.jeanlucmakiola.agendula.ui.accounts -import android.content.ActivityNotFoundException import android.content.Intent import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.foundation.layout.Arrangement @@ -96,12 +95,19 @@ internal fun AddAccountScreen( LaunchedEffect(state.openInBrowser) { val url = state.openInBrowser ?: return@LaunchedEffect val uri = url.toString().toUri() - try { + // ⚠️ Neither failure may escape, and neither may be ignored. An + // exception out of a LaunchedEffect takes the app down — and the outer + // catch named only ActivityNotFoundException, so a SecurityException + // from a locked-down profile did exactly that. Reporting the failure is + // the other half: clearing openInBrowser regardless left the user on + // "waiting for your browser" with a spinner and no browser, on the very + // devices this fallback exists for. + val launched = runCatching { CustomTabsIntent.Builder().build().launchUrl(context, uri) - } catch (_: ActivityNotFoundException) { - runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, uri)) } - } - viewModel.onBrowserLaunched() + }.recoverCatching { + context.startActivity(Intent(Intent.ACTION_VIEW, uri)) + }.isSuccess + if (launched) viewModel.onBrowserLaunched() else viewModel.onBrowserUnavailable() } // Backing out abandons a flow that may still be polling the server every two @@ -349,6 +355,8 @@ private fun AddAccountMessage.text(): String = when (this) { stringResource(R.string.add_account_browser_error_maintenance) AddAccountMessage.BrowserFailed -> stringResource(R.string.add_account_browser_error_failed) + AddAccountMessage.BrowserUnavailable -> + stringResource(R.string.add_account_browser_error_unavailable) is AddAccountMessage.OutsideCredentialScope -> stringResource(R.string.add_account_error_cross_domain, host) is AddAccountMessage.Quirk -> when (quirk) { diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountViewModel.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountViewModel.kt index af48af0..0973cd2 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountViewModel.kt @@ -392,6 +392,21 @@ class AddAccountViewModel @Inject constructor( fun onBrowserLaunched() = _state.update { it.copy(openInBrowser = null) } + /** + * Nothing could open the URL, so there is no approval coming. + * + * ⚠️ Without this, a failed launch still cleared `openInBrowser` and left the + * user watching "waiting for your browser" for the rest of a twenty-minute + * window, on exactly the browserless devices the Custom Tabs fallback exists + * for. Nothing was minted — the flow was never opened — so there is nothing + * to hand back; the poll is stopped because it is polling for an approval + * that cannot happen. + */ + fun onBrowserUnavailable() { + pollJob?.cancel() + browserFailed(AddAccountMessage.BrowserUnavailable) + } + fun onListToggled(url: HttpUrl) { val step = _state.value.step as? AddAccountStep.ChooseLists ?: return val selected = if (url in step.selected) step.selected - url else step.selected + url diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92c9a9d..1967390 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -348,6 +348,7 @@ The server is turning away repeated attempts. Wait a few minutes and try again. The server is in maintenance mode. Try again once it is back. The server didn\u2019t finish signing you in. Try again in a moment. + This device has no browser that can open the sign-in page. Use a password instead. Fastmail needs an app password, not your account password \u2014 and CalDAV is not on the Basic plan. iCloud needs an app-specific password, which you create at appleid.apple.com with two-factor on. Fastmail needs an app password, and CalDAV is not available on the Basic plan.