Repository navigation
android: don't let repeated login taps cancel each other; surface failures - #895
Open
denisulmer wants to merge 1 commit into
Open
denisulmer wants to merge 1 commit into
denisulmer wants to merge 1 commit into
Conversation
…lures Tapping "Log in" (or "Reauthenticate") could appear to do nothing: -Every IpnViewModel.login() call runs editPrefs() + start(). In the Go backend, start() calls resetControlClientLocked(), which cancels the in-flight register request and clears the cached auth URL. Repeated taps (or a tap racing another start) therefore keep cancelling the request that would produce the BrowseToURL, which is the "TryLogin: register request: context canceled" seen in the logs. -Failures in the login chain were only logged; there was no feedback and no timeout when no auth URL arrived. -Notifier.browseToURL is a StateFlow that was only cleared after a successful login, so the backend re-sending the same URL (which StartLoginInteractive does for a still-valid URL) was conflated away and no browser opened. -The browseToURL collector ran regardless of lifecycle, so a URL that arrived while the app was in the background tried to start the browser from the background, which Android silently blocks. This change: -Tracks the login phase across view models. A plain interactive login requested while one is in progress no longer calls editPrefs()/start(): it is ignored while start() is in flight, and afterwards only calls startLoginInteractive() to re-request the URL. -Arms a watchdog: if neither an auth URL nor a completed login is seen within 30s, the login is reset and the user is told. Failures of the interactive login chain are shown as a toast. -Collects browseToURL only while MainActivity is STARTED and consumes the URL after launching the browser, so the same URL can be delivered again and is not reopened on activity restart. A counter in Notifier lets the watchdog see repeated URLs. -Shows a message when no browser can open the login URL. Updates tailscale/tailscale#18304 Signed-off-by: Denis Ulmer <22558132+denisulmer@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Updates tailscale/tailscale#18304
Problem
Since late 2025 many users report that tapping Log in in the Android app does nothing (#18304, 45+ comments, Pixel/Samsung/OnePlus/Android TV/ChromeOS, Android 8–17, app 1.90.9 through 1.98.8). #746 and #747 (shipped in 1.96.0) fixed one race, but reports continue on 1.96.4 (1, 2, 3) and 1.98.8 (4).
Workarounds that users report as working all point at timing or retry problems rather than one specific browser: tapping repeatedly (jahinzee, ChenJiasen-123), force-stopping or rebooting (mwdle), connect/disconnect loops (Walsh-Colin), and the login page showing up late (Jebble). Users behind DPI firewalls also see the button "do nothing" because errors are never shown (erikGav).
Root cause
IpnViewModel.login()runseditPrefs()→start()→startLoginInteractive(). In the backend,Start()callsresetControlClientLocked()(ipn/ipnlocal/local.go), which cancels the in-flight register request and clearsauthURL. A second tap (Log in, Reauthenticate, add account, or a concurrent start) before the URL arrives therefore cancels the request that would have produced it. That is thecontrol: TryLogin: register request: context canceledin the logs posted on the issue. android: fix silent login failure and double browser #746 removed one source of this but not repeatedlogin()calls.MainViewcallsviewModel.login()with the default no-op completion handler.editPrefs/start/startLoginInteractiveerrors and browser-launch errors were only logged, and there was no timeout if noBrowseToURLarrived.Notifier.browseToURLis aStateFlowthat was only cleared after a successful login.StartLoginInteractivere-sends the cached URL when it is still valid; that identical value was conflated by theStateFlow, so no browser opened.browseToURLcollector inMainActivity.initran regardless of lifecycle. A URL that arrived while the activity was stopped (slow network, VPN dialog, user switched away) triedstartActivityfrom the background, which Android 10+ blocks silently.Changes
IpnViewModel.login(): a login phase (IDLE/STARTING/AWAITING_URL) shared across view models. A plain interactive login requested while one is in progress no longer callseditPrefs()/start(). It is ignored whilestart()is in flight, and after that it only callsstartLoginInteractive()to re-request the URL. Auth-key and custom-control-URL logins keep their existing behaviour.BrowseToURLnor a transition to Running/NeedsMachineAuth arrives within 30s, the phase is reset (so the next tap restarts cleanly) and a toast explains it. Failures in the interactive login chain also show a toast. Auth-key and custom-URL screens keep their existing error dialogs.Notifier: adds abrowseToURLCountthat increments on everyBrowseToURLnotification, so repeated identical URLs are still observable after the URL has been consumed.MainActivity: thebrowseToURLcollector usesrepeatOnLifecycle(STARTED)and clears the URL after launching the browser (not on Android TV, where the QR view still uses it). A toast is shown when neither Custom Tabs nor a plainACTION_VIEWcan open the URL.login_failed,login_timeout,login_no_browser.Testing
make tailscale-debug.apk: builds (JDK 21, SDK platform 36, build-tools 36.0.0, NDK 23.1.7779620, Linux x86_64).make fmt-check(ktfmt): passes.make test(JVM unit tests): passes.make release-lint(lintVitalRelease): passes.Limitations / follow-ups
IpnViewModeldepends onApp.get()and the LocalAPI, and there is no existing test harness for the login flow.startForegroundForLogin()uses thesystemExemptedtype before VPN consent and may fail (only logged) on Android 14+. That is worth a separate look.