diff --git a/docs/manual-checks.md b/docs/manual-checks.md
index 911d665..ea206ea 100644
--- a/docs/manual-checks.md
+++ b/docs/manual-checks.md
@@ -1750,15 +1750,18 @@ tap of an arrow key.
With a shell open and the software keyboard up, tap **esc**, **tab** or an arrow on the accessory row, then
keep typing on the software keyboard.
-**Pass:** the keyboard does not change — not its layout, not its suggestion strip, not its height — and
+**Pass:** the keyboard settles back unchanged — same layout, same suggestion strip, same height — and
everything typed after the tap still reaches the terminal. The accessory row stays visible above the
-keyboard throughout.
+keyboard throughout. A blink during the press itself is tolerable: the platform takes the focus on both
+halves of every touch and the return is posted right behind each theft, so the connection can visibly flap
+for the press's own duration — what it must never do is *stay* swapped after the finger lifts.
**Failure means:** Android's own view focus stayed on Avalonia's input view after the tap instead of being
-handed back. This is the half `Focusable = false` cannot reach — the platform moves its focus on the touch
-itself, before Avalonia decides anything — and the symptom chain is the keyboard swapping to its no-input
-layout and the inset churn parking it over the very row that was tapped. See
-`TerminalFocus` in the Android head's Platform folder.
+handed back. This is the half `Focusable = false` cannot reach — the platform requests focus for its own
+view after dispatching every handled touch — and the symptom chain is the keyboard swapping to its no-input
+layout and the inset churn parking it over the very row that was tapped. The first fix for this failed by
+timing alone: it handed focus back from inside the very dispatch the platform re-steals it after. See
+`TerminalFocus` in the Android head's Platform folder for both the mechanism and the fix's shape.
### 11.11 Closing a connection and opening a new one both take you somewhere real
diff --git a/src/DodoSSH.Client.Android/Platform/TerminalFocus.cs b/src/DodoSSH.Client.Android/Platform/TerminalFocus.cs
index a2d4617..97b929e 100644
--- a/src/DodoSSH.Client.Android/Platform/TerminalFocus.cs
+++ b/src/DodoSSH.Client.Android/Platform/TerminalFocus.cs
@@ -10,17 +10,24 @@ namespace DodoSSH.Client.Android.Platform;
/// The sibling of , and it exists for the same reason that one does: the
/// keyboard over a terminal belongs to the WebView's own native view, which Avalonia's focus manager does
/// not own. The accessory row's keys are already Focusable=false — see TerminalScreen — so Avalonia's
-/// idea of focus never leaves the terminal when one is tapped. What still moves is Android's: the
-/// tap lands on Avalonia's own input view, which is focusable-in-touch-mode because Avalonia's text boxes
-/// need it to be, and the platform hands that view focus on the way to delivering the touch. The WebView's
-/// input connection dies with its focus, the keyboard swaps to the layout it shows an editor that takes no
-/// text, and the inset churn that follows can leave it sitting on top of the very row that was tapped.
+/// idea of focus never leaves the terminal when one is tapped. What still moves is Android's:
+/// AvaloniaView.DispatchTouchEvent (decompiled from Avalonia.Android 12.1.1) ends every handled
+/// touch — DOWN and UP alike — with a RequestFocus() for Avalonia's own view. The WebView's input
+/// connection dies with its focus, the keyboard swaps to the layout it shows an editor that takes no text,
+/// and the inset churn that follows can leave it sitting on top of the very row that was tapped.
///
///
-/// So each accessory key hands focus straight back once its byte is on the wire. The page inside the
-/// WebView never noticed anything — its own DOM focus never moved — so regaining native focus re-establishes
-/// the same input connection and the keyboard settles back to what it was. Skipped when the WebView is still
-/// focused, which makes the call free on any platform arrangement where the steal never happened.
+/// Posted, not called — the posting is the fix's second attempt, and the first one's failure is why.
+/// The first version called RequestFocus() from the keys' own Click handlers, which fire
+/// inside the UP event's dispatch — and the platform's own request runs after dispatch
+/// returns, so it undid ours a few microseconds later and the terminal stayed unfocused. A posted runnable
+/// runs on the next main-looper message, after the platform has taken its turn, so ours is the request that
+/// sticks. The focus check lives inside the posted runnable for the same reason: the answer at call time is
+/// about to be made stale by the very mechanism this exists to counter.
+///
+///
+/// The page inside the WebView never noticed any of this — its own DOM focus never moved — so regaining
+/// native focus re-establishes the same input connection and the keyboard settles back to what it was.
///
///
/// Found by walking the decor view rather than asked of the NativeWebView control, because the
@@ -39,10 +46,18 @@ internal static class TerminalFocus
return;
}
- if (FindWebView(decor) is { IsFocused: false } webView)
+ if (FindWebView(decor) is not { } webView)
{
- webView.RequestFocus();
+ return;
}
+
+ webView.Post(() =>
+ {
+ if (!webView.IsFocused)
+ {
+ webView.RequestFocus();
+ }
+ });
}
private static View? FindWebView(ViewGroup parent)
diff --git a/src/DodoSSH.Client.Android/Theme/Phone.axaml b/src/DodoSSH.Client.Android/Theme/Phone.axaml
index 5ee8c3c..725e1dd 100644
--- a/src/DodoSSH.Client.Android/Theme/Phone.axaml
+++ b/src/DodoSSH.Client.Android/Theme/Phone.axaml
@@ -116,11 +116,27 @@
-
+