From 9e4f33a31baf146203799b66818dec93c62a117b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Sep 2026 11:54:59 -0400 Subject: [PATCH] fix(embed): stop RemoteImeView taking focus from the rest of the app RemoteImeView, the hidden EditText that hosts the keyboard for embedded tabs, lives in the main window whether or not a browser tab is open, and was always focusable in touch mode. So it was the fallback focus target for the whole window: whenever a Compose text field lost focus (clearFocus(), or its screen being popped), Android gave focus to it, it became the IME target, and the keyboard stayed up over a screen with no field. It also reopened on every resume. Reproduced on main (SM-T220, Android 14): focus the Settings search, tap back. The Home feed keeps the keyboard up with RemoteImeView as the served view, and it is still up after Home -> resume. Happens without ever opening a browser tab. It is now focusable only while it mirrors a page field: onPageFocus turns focus on, onPageBlur turns it off (which also clears its focus). Both flags are set so a hardware keyboard or D-pad can't focus it either. Device-verified on the fixed build: the repro leaves the keyboard down (served view goes back to AndroidComposeView) across two runs; in an embedded tab, typing, dismiss + re-tap, a Home round trip mid-typing (keyboard and caret restored), and a page-side blur() all still work. Co-Authored-By: Claude Opus 5.5 --- .../ui/screen/loggedIn/embed/RemoteImeView.kt | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/RemoteImeView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/RemoteImeView.kt index 047fedb698..aed8de884d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/RemoteImeView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/RemoteImeView.kt @@ -90,9 +90,10 @@ class RemoteImeView( private val flush = Runnable { flushState() } init { - // Invisible but focusable: the IME needs a laid-out, visible target, but the user must never see - // this field or its cursor/selection handles — only the embedded page. - isFocusableInTouchMode = true + // Invisible: the IME needs a laid-out, visible target, but the user must never see this field or its + // cursor/selection handles — only the embedded page. Focusable only while it mirrors a page field + // (see [setFocusTarget]). + setFocusTarget(false) alpha = 0f background = null setTextColor(0x00000000) @@ -186,6 +187,7 @@ class RemoteImeView( ) { configureFor(focus) mirroring = true + setFocusTarget(true) fieldReadOnly = focus.readOnly // Focus the EditText BEFORE seeding text/selection. An EditText jumps its caret to the end when it // gains focus; if we seed first, that end-position then overrides the seed and gets shipped to the @@ -313,12 +315,28 @@ class RemoteImeView( // whatever arrives to the field focused THEN, not the one it was computed from. Nothing this mirror // holds belongs to the page once the field has blurred, so suppress the echo and drop the queue. applyingRemote = true - clearFocus() + // Not focusable again until the next page focus. Dropping the flag clears our focus as well, so no + // separate clearFocus() is needed (and one alone could hand focus straight back to us). + setFocusTarget(false) applyingRemote = false removeCallbacks(flush) imm.hideSoftInputFromWindow(windowToken, 0) } + /** + * Whether this view can take focus. It lives in the main window whether or not a browser tab is open, so + * while it is focusable it is the fallback focus target for the whole window: when a Compose text field + * elsewhere loses focus (clearFocus(), or its screen being popped), Android hands focus to this EditText. + * It then becomes the IME's target, so the keyboard stays up over a screen with no field, and it comes + * back every time the app resumes. So it is focusable only while it actually mirrors a page field. + * Both flags are set: `isFocusable` alone would still let it take focus outside touch mode (a hardware + * keyboard or D-pad). + */ + private fun setFocusTarget(enabled: Boolean) { + isFocusable = enabled + isFocusableInTouchMode = enabled + } + private fun applyRemote( newText: String, selStart: Int,