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 @@ + + + + + + + + + + + + + + + + + + + +