diff --git a/docs/android-port.md b/docs/android-port.md index c3795c8..e07119a 100644 --- a/docs/android-port.md +++ b/docs/android-port.md @@ -562,3 +562,16 @@ Recorded so they are choices rather than accidents. Any of them is cheap to revi - **`NativeKeyboardFocus` is not ported.** It exists for a documented Win32 asymmetry — focus crosses into WebView2 but does not come back — and Android's focus model is different enough that the problem should be confirmed to exist before anything is written to solve it. +- **The software keyboard is kept off the interface in one place, and by two mechanisms.** `PhoneShell` + owns it rather than each screen, because everything the phone draws is inside that one control and a + screen added later would otherwise have to remember. The two mechanisms are not a belt and braces: before + Android 15 the activity's `AdjustResize` has the platform shorten the window and the reported keyboard + inset arrives already consumed, and from Android 15 edge-to-edge is enforced, the window is no longer + resized for the keyboard, and the inset is what there is. Each is dead where the other applies, which is + why the margin is taken from the inset alone — the two added together would strand the interface an + entire keyboard too high. See `docs/manual-checks.md` phase 10, which is the only way either is verified. +- **A box that takes a secret says so twice.** `PasswordChar` is what the screen draws and + `TextInputOptions.ContentType` is what the software keyboard is told, and only the second one turns off + the suggestion strip and keeps the passphrase out of the IME's learning dictionary. The desktop head + needs only the first, which is why the phone's `TextBox.secret` class carries both rather than the two + being set per box. diff --git a/docs/manual-checks.md b/docs/manual-checks.md index 53bcd9e..0acef2e 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -834,3 +834,58 @@ nothing else about them changes. Neither host is queued for push. On the phone's host editor with several tags: the chips are at least 36 tall, spaced enough that a miss lands between them rather than on the wrong tag, and the new-tag box and ADD sit on one row without either being squeezed to nothing. + +--- + +## Phase 10 — The software keyboard and the boxes that take secrets + +Every check here needs a real Android device or emulator, and there is no headless equivalent of any of +them: the software keyboard is an inset the platform reports, and a headless top level reports none. +Worth running on two devices if you have them — one on Android 14 or earlier and one on Android 15 or +later — because the interface is kept clear of the keyboard by a different mechanism on each. Before 15 the +activity's `AdjustResize` has the platform shorten the window; from 15 the window is not resized at all and +`PhoneShell` applies the reported inset itself. A build that only ever ran on one of the two will look +correct and be half broken. + +### 10.1 The vault passphrase box is treated as a password by the keyboard + +Launch to the lock screen, tap the passphrase box, type a few characters. + +**Pass:** dots on screen, and **no suggestion strip above the keyboard** — no completions, no previously +typed words, no autocorrect. Then open any ordinary box on the phone (the host search, a snippet's name) +and confirm the suggestions come back there. + +**Failure means:** `TextInputOptions.ContentType` is missing — most likely a box was given `PasswordChar` +directly instead of `Classes="... secret"`. `PasswordChar` is what the screen draws; the content type is +what the keyboard is told, and only the second one keeps a passphrase out of the IME's learning +dictionary. A box showing dots with a suggestion strip over it is the worst case, not a cosmetic one. + +### 10.2 The keyboard does not cover the box being typed into + +The same box: with the keyboard up, the passphrase box and the UNLOCK button under it are both visible. +Repeat on each of the five boxes that take a secret — lock screen, both enrollment boxes, the connect +password on HOSTS, and the connect password on FILES. + +**Pass:** the box stays on screen when the keyboard opens, and the interface is shortened rather than slid +— the header stays where it is rather than scrolling off the top. + +**Failure means:** on Android 15 or later, the inset is no longer reaching `PhoneShell`. On 14 or earlier, +`WindowSoftInputMode` has been dropped from the activity and the platform is panning the window instead of +resizing it — which, for a window whose whole content is one native view, pans by nothing useful. + +### 10.3 Nothing is stranded when the keyboard closes + +Dismiss the keyboard with back or the down-chevron from each of those screens. + +**Pass:** the interface fills the screen again immediately, with no band of empty canvas left along the +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.4 Rotating with the keyboard up + +Focus a passphrase box, then turn the phone sideways. + +**Pass:** the box is still visible and still focused, and the shell is intact — the activity handles the +rotation rather than being recreated, and live shells survive it. diff --git a/src/DodoSSH.Client.Android/MainActivity.cs b/src/DodoSSH.Client.Android/MainActivity.cs index a61b308..02cbcbb 100644 --- a/src/DodoSSH.Client.Android/MainActivity.cs +++ b/src/DodoSSH.Client.Android/MainActivity.cs @@ -8,6 +8,7 @@ using DodoSSH.Client.Android.Platform; using global::Android.App; using global::Android.Content; using global::Android.Content.PM; +using global::Android.Views; namespace DodoSSH.Client.Android; @@ -34,12 +35,26 @@ namespace DodoSSH.Client.Android; /// other launch mode answers it with a second copy of this activity on top of the first — which on this /// head would mean a second Avalonia application over a live one. /// +/// +/// AdjustResize is declared rather than left unspecified, and it is half of how this head +/// keeps the software keyboard off the box being typed into; PhoneShell is the other half. Left +/// unspecified, Android chooses, and what it chooses for a window whose entire content is one native view +/// — which is what an Avalonia surface is — is to pan: it slides the window up by however much it thinks +/// the focused *native* view needs, and since that view is the whole surface, the passphrase box goes on +/// sitting under the keyboard. Resizing instead makes the window shorter, which the layout inside it can +/// answer, and a screen built around a ScrollViewer then scrolls the focused box into view by +/// itself. On Android 15 and later this attribute is ignored — edge-to-edge is enforced there and the +/// window is no longer resized for the keyboard — which is precisely the case PhoneShell handles from the +/// reported inset. The two are complementary and never both in effect: where the window resizes, the +/// keyboard inset arrives already consumed and measures zero. +/// /// [Activity( Label = "DodoSSH", Theme = "@style/DodoTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTask, + WindowSoftInputMode = SoftInput.AdjustResize, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.ScreenLayout diff --git a/src/DodoSSH.Client.Android/Theme/Phone.axaml b/src/DodoSSH.Client.Android/Theme/Phone.axaml index d27fcd7..8be44bb 100644 --- a/src/DodoSSH.Client.Android/Theme/Phone.axaml +++ b/src/DodoSSH.Client.Android/Theme/Phone.axaml @@ -320,6 +320,27 @@ + + + - +