diff --git a/docs/manual-checks.md b/docs/manual-checks.md
index 8f10ba2..a009239 100644
--- a/docs/manual-checks.md
+++ b/docs/manual-checks.md
@@ -1810,6 +1810,27 @@ bottom and no scroll position left part way down.
**Failure means:** the inset is being applied but not cleared — the closed state is not being read from the
event, or the margin is only ever added to.
+### 10.4a Nothing is stranded when the keyboard is still up on the way *out* · **the one 10.4 missed**
+
+Focus any box, and with the keyboard still on screen leave the application — Home, or the task switcher,
+or anything that puts another app in front. Come back to it.
+
+**Pass:** the interface fills the screen. Do it from the terminal too, where the keypad and the shell should
+both be where they were.
+
+**Failure means:** `PhoneShell` is not re-reading the inset on the way back in. This is a separate path from
+10.4 and it is the one that has actually shipped broken: Android takes the keyboard away when the activity
+stops and Avalonia's input pane raises no `StateChanged` for it, because the pane goes with the window
+rather than closing in front of one. So the last value applied was for a keyboard that was up, and the
+application returns holding a whole keyboard's height of empty canvas — measured at 838 of 2340 pixels on a
+Pixel 5, on every screen, and staying that way until something opens and closes the keyboard. See
+`PhoneShell.OnActivityResumed` and `PhoneEnvironment.Resumed`.
+
+Worth doing in both directions: a resume that *should* keep the inset is the other half. Trigger a
+permission prompt or the biometric sheet with the keyboard up, dismiss it, and check the box being typed
+into is still clear of a keyboard that is genuinely still there — a fix that flattened the inset on every
+resume would pass the paragraph above and fail this one.
+
### 10.5 Rotating with the keyboard up
Focus a passphrase box, then turn the phone sideways.
@@ -2060,6 +2081,25 @@ cause would look unrelated. See the correction in [ADR 0015](adr/0015-android-te
Worth pressing Enter on an empty prompt a few times too: each one should give a fresh prompt and nothing
else. A doubled prompt per press means a line ending is arriving as both CR and LF.
+### 11.10f Every key on the keypad is there, and pressing one presses it
+
+With a shell open, look at the two rows above the keyboard without touching anything: **esc tab ctrl | - ~ /**
+on the upper row and **← ↓ ↑ →** with **A−** and **A+** on the lower one. All thirteen, at once, on a 360dp
+phone in portrait.
+
+Then press them with a thumb rather than a fingertip, at an angle, twenty or so times across both rows —
+including the keys at the far left and far right ends.
+
+**Pass:** every press registers, the key darkens under the thumb while it is down, and **nothing slides
+sideways**. The row is exactly where it was after twenty presses as it was before the first.
+
+**Failure means:** a key that misses is a target that has got smaller — `Button.hotkey` is 40 tall for a
+reason and Android asks for 48. A row that *moves* is the one that matters: it means something has put a
+`ScrollViewer` back around the keys. That is how this row shipped originally and it does not survive contact
+with a thumb — a pan and a press begin with the same few pixels of travel, the scroller claims them, and the
+press is never delivered. If the set of keys outgrows the width, add a third row; do not make it scroll.
+See the keypad's own remark in `TerminalScreen.axaml`.
+
### 11.11 Closing a connection and opening a new one both take you somewhere real
Open a shell, close its tab, then open a different one from HOSTS.
diff --git a/src/DodoSSH.Client.Android/MainActivity.cs b/src/DodoSSH.Client.Android/MainActivity.cs
index f1b7134..c7e111c 100644
--- a/src/DodoSSH.Client.Android/MainActivity.cs
+++ b/src/DodoSSH.Client.Android/MainActivity.cs
@@ -90,6 +90,11 @@ public sealed class MainActivity : AvaloniaMainActivity
PhoneEnvironment.CurrentActivity = this;
AndroidRedirectCallback.Abandon();
+
+ // And the keyboard may have gone away while this activity was not on screen, without Avalonia's
+ // input pane raising anything to say so. PhoneShell re-reads the inset from this. See
+ // PhoneEnvironment.Resumed, which carries the whole of why.
+ PhoneEnvironment.RaiseResumed();
}
///
diff --git a/src/DodoSSH.Client.Android/Platform/PhoneEnvironment.cs b/src/DodoSSH.Client.Android/Platform/PhoneEnvironment.cs
index 58b204a..bd9eb06 100644
--- a/src/DodoSSH.Client.Android/Platform/PhoneEnvironment.cs
+++ b/src/DodoSSH.Client.Android/Platform/PhoneEnvironment.cs
@@ -100,6 +100,35 @@ internal static class PhoneEnvironment
///
public static global::Android.App.Activity? CurrentActivity { get; set; }
+ /// Raised each time the launcher activity comes back to the foreground.
+ ///
+ ///
+ /// ◆ Exists because the software keyboard can go away without Avalonia being told.
+ /// IInputPane.StateChanged is the only signal PhoneShell has for the keyboard opening and
+ /// closing, and it is not raised when Android dismisses the keyboard on the way out of the app — the
+ /// activity is being paused, and the pane is simply gone by the time anything could have reported it.
+ /// So the inset that holds the interface clear of the keyboard was still applied on the way back in,
+ /// and the whole phone drew an entire keyboard's height above the bottom of a screen with no keyboard
+ /// on it. Reproduced every time: open the keyboard anywhere, press Home, return — 838 of 2340 pixels
+ /// dead, and staying that way until the keyboard was opened and closed again.
+ ///
+ ///
+ /// An event here rather than a call into the view, because the view is Avalonia's and this is Android's:
+ /// MainActivity already owns OnResume and already writes
+ /// from it, and a static event is the same shape of hand-off one step further. Anything else that needs
+ /// to re-read platform state on the way back in can subscribe without MainActivity learning what
+ /// it is.
+ ///
+ ///
+ /// Subscribers are responsible for unsubscribing — this is a static event and holds whatever it is
+ /// given for the life of the process. PhoneShell attaches and detaches with its visual tree.
+ ///
+ ///
+ public static event EventHandler? Resumed;
+
+ /// Announces that the launcher activity is foreground again.
+ internal static void RaiseResumed() => Resumed?.Invoke(null, EventArgs.Empty);
+
/// The application context, once has run.
public static Context Require() =>
context ?? throw new InvalidOperationException(
diff --git a/src/DodoSSH.Client.Android/Theme/Phone.axaml b/src/DodoSSH.Client.Android/Theme/Phone.axaml
index ee4fcf5..fb3f0d7 100644
--- a/src/DodoSSH.Client.Android/Theme/Phone.axaml
+++ b/src/DodoSSH.Client.Android/Theme/Phone.axaml
@@ -167,10 +167,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+