fix(captcha): fall back to external browser when system WebView is disabled (refs #303)#736
Conversation
…ollyim#303) Refs mollyim#303 Users on low-storage devices and some hardened ROMs disable or remove the system WebView. Today that bricks registration because `CaptchaFragment` unconditionally tries to load the captcha into a WebView and either silently fails to render or crashes when the WebView constructor / inflate throws. Wrap the WebView setup in a try/catch. If the WebView is not available, fall back to opening `BuildConfig.SIGNAL_CAPTCHA_URL` in the user's default browser. The existing `signalcaptcha://` scheme handler (used by the share-sheet today) brings the user back into the app with the token and registration completes normally. If no browser is available either we just navigate up so the user can retry rather than hanging on a blank screen.
There was a problem hiding this comment.
Pull request overview
This PR aims to prevent registration from breaking on devices where the system WebView provider is disabled/uninstalled by falling back to opening the CAPTCHA page in an external browser.
Changes:
- Wrap WebView access/configuration and
loadUrl()in try/catch to detect WebView failures. - Add a fallback path that launches
BuildConfig.SIGNAL_CAPTCHA_URLviaIntent.ACTION_VIEWwhen WebView is unavailable. - Navigate up from the CAPTCHA screen after triggering the fallback behavior.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private fun openCaptchaInBrowser() { | ||
| val intent = Intent(Intent.ACTION_VIEW, Uri.parse(BuildConfig.SIGNAL_CAPTCHA_URL)) | ||
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||
| try { | ||
| startActivity(intent) | ||
| } catch (e: ActivityNotFoundException) { | ||
| // No browser available; let the user back out. | ||
| } | ||
| binding.registrationCaptchaWebView.loadUrl(BuildConfig.SIGNAL_CAPTCHA_URL) | ||
| findNavController().navigateUp() | ||
| } |
There was a problem hiding this comment.
openCaptchaInBrowser() always calls findNavController().navigateUp() even when the browser intent launches successfully. This doesn’t match the PR description (“If no browser is available either we just navigateUp()”) and can also make the UI jump back immediately after launching the browser. Consider only navigating up in the ActivityNotFoundException case, or making this behavior explicit in the PR description and UX (e.g., show a message / button while waiting for the token).
|
|
||
| private fun openCaptchaInBrowser() { | ||
| val intent = Intent(Intent.ACTION_VIEW, Uri.parse(BuildConfig.SIGNAL_CAPTCHA_URL)) | ||
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |
There was a problem hiding this comment.
Intent.FLAG_ACTIVITY_NEW_TASK is typically only needed when starting an activity from a non-Activity context. Here you’re calling Fragment.startActivity(), so the flag is unnecessary and may change task/back-stack behavior. Consider removing it, or only adding it conditionally (similar to CommunicationActions.openBrowserLink(), which only sets NEW_TASK when the Context is not an Activity).
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |
| try { | ||
| webView.settings.javaScriptEnabled = true | ||
| webView.clearCache(true) | ||
|
|
||
| webView.webViewClient = object : WebViewClient() { | ||
| @Deprecated("Deprecated in Java") | ||
| override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { | ||
| if (url.startsWith(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME)) { | ||
| val token = url.substring(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME.length) | ||
| handleCaptchaToken(token) | ||
| findNavController().navigateUp() | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
| return false | ||
| } | ||
| webView.loadUrl(BuildConfig.SIGNAL_CAPTCHA_URL) | ||
| } catch (e: Throwable) { | ||
| // Catches AndroidRuntimeException("WebView ... not available") thrown | ||
| // by the WebView constructor / settings when the WebView package is | ||
| // disabled at runtime. | ||
| openCaptchaInBrowser() | ||
| } |
There was a problem hiding this comment.
Catching Throwable here is overly broad (it will also catch serious Errors like OOM, linkage errors, etc.) and can mask unrelated bugs. Since the intent is to detect “WebView provider not available”, prefer catching the specific runtime exceptions thrown by WebView when the provider is missing (e.g., AndroidRuntimeException / InflateException depending on where handled) or at least Exception/RuntimeException rather than Throwable.
| // Issue #303: users that have disabled / removed the system WebView | ||
| // (low-storage devices, hardened ROMs) cannot complete the captcha | ||
| // because the WebView either fails to inflate or refuses to load. | ||
| // Detect that case and fall back to opening the captcha URL in the | ||
| // user's default browser — the `signalcaptcha://` URI scheme handler | ||
| // brings them back into the app with the token via the existing | ||
| // intent filter. | ||
| val webView = try { | ||
| binding.registrationCaptchaWebView | ||
| } catch (e: Throwable) { | ||
| openCaptchaInBrowser() | ||
| return | ||
| } |
There was a problem hiding this comment.
Catching errors in onViewCreated won’t prevent the crash reported in #303 when WebView inflation fails: fragment_registration_captcha.xml instantiates directly, so devices without a WebView provider can throw an InflateException before onViewCreated runs. To make this fallback effective, the WebView needs to be inflated/created inside a try/catch (e.g., override onCreateView and fall back to a non-WebView layout / ViewStub / programmatically-added WebView) so the fragment can render and open the browser instead of crashing.
| // Detect that case and fall back to opening the captcha URL in the | ||
| // user's default browser — the `signalcaptcha://` URI scheme handler | ||
| // brings them back into the app with the token via the existing | ||
| // intent filter. |
There was a problem hiding this comment.
The browser fallback assumes the captcha page can redirect back into the app via signalcaptcha://…, but this repo doesn’t appear to declare any manifest intent-filter for the signalcaptcha scheme (app/src/main/AndroidManifest.xml only registers sgnl, tsdevice, and https). Without a scheme handler, completing the captcha in an external browser won’t be able to return the token to the app. Either add an exported activity intent-filter for signalcaptcha:// that forwards the token into the registration flow, or change the captcha URL/return scheme to one that is already handled (e.g., existing sgnl:// deep link handling).
Open captcha in the default browser when WebView is disabled (refs #303)
Problem
Issue #303 reports that disabling / removing the system
WebView (a common configuration on low-storage devices and
some hardened ROMs) bricks registration:
CaptchaFragmentunconditionally calls into the WebView and either silently
fails to render or crashes when the WebView constructor
throws
AndroidRuntimeException("WebView ... not available").Fix
Wrap the WebView access in a try/catch. If accessing the
WebView (
binding.registrationCaptchaWebViewand itssettings) throws, or ifloadUrlthrows, fall back tolaunching
BuildConfig.SIGNAL_CAPTCHA_URLviaIntent.ACTION_VIEW. The existingsignalcaptcha://URIscheme handler (the same one the share-sheet uses today)
brings the user back into the app with the token and
handleCaptchaTokenruns as normal.If no browser is available either we just
navigateUp()sothe user can back out and retry, no worse than the current
blank-screen behaviour.
Why this is in scope of
mollyim-androidUpstream Signal does not have to care because it does not
ship a browser-light user base. The mollyim user base has
explicitly asked for it (#303) and the change is
self-contained inside the fork's existing CaptchaFragment
(no AndroidManifest or shared-component edits).
Notes
signalcaptcha://is alreadydeclared (it is what makes the share-sheet workflow
function today), so the round-trip through the browser
works without any manifest changes.
clearCache(true)is preserved on the WebView path; thebrowser path does not need it because the user's browser
manages its own cache.