Public Access
2d95366552b387445a8fa8b5440d0bc9b97c389e
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
aff776cb42 |
Rebuild the terminal's keypad, and fix what a device run showed
Six defects, all found by running this head on an emulator against a local API rather than by reading it. They are in one commit because two of the files carry several of them and splitting those would mean hand-editing patches; each is argued at its own site. ── the keypad, which is most of this ─────────────────────────────────────────────────────────────────── It was eleven 30-by-44 keys in a horizontal ScrollViewer, and it was wrong twice over. A pan and a press begin with the same few pixels of travel, so a thumb coming down at an angle — which is how a thumb comes down — was read as the start of a pan and the press was never delivered; the row slid slightly instead. And 30 tall is well under the 48 Android asks for, so the presses that did arrive were hard to aim. The arrows, the keys pressed most and in sequence, were the ones parked past the right-hand edge. Termux, Termius, JuiceSSH, ConnectBot and Blink all ship this row and not one of them scrolls it: they divide the width evenly and add a second row when the set does not fit. That is not convention for its own sake — a key whose position is fixed can be hit without looking, which is the whole point of a modifier row used while reading the output above it. So: two UniformGrid rows, every key an equal share of the width, nothing scrollable, 40 tall. Every key kept. Arrows on the lower row where the thumb rests, reordered to ← ↓ ↑ → so the pairs that undo each other are neighbours. It costs about 51 pixels of terminal, four or five lines, and that is the right trade: the row that was cheaper did not work. The keys also gain a pressed state, which they had none of. They are Focusable="False" so that using one does not take the keyboard off the terminal, and a button that never takes focus never shows the adorner Fluent would otherwise give it — leaving no feedback at all on a control whose only job is to be pressed. Their looks move from assignments in the code-behind to Button.hotkey, which is what lets a style say :pressed, and the Ctrl latch becomes a class rather than two brushes written by hand. ── and five smaller ones ──────────────────────────────────────────────────────────────────────────────── The keyboard's inset was never cleared when the app was resumed without the keyboard it was applied for. Android takes the keyboard away as the activity stops and Avalonia's input pane raises no StateChanged, the pane going with the window rather than closing in front of one — so the interface came back holding a whole keyboard's height of empty canvas. 838 of 2340 pixels on a Pixel 5, on every screen, until something opened and closed the keyboard. MainActivity now raises PhoneEnvironment.Resumed and PhoneShell re-asks the pane. It asks rather than assuming zero: a permission prompt or the biometric sheet can hand the activity back with the keyboard genuinely still up, and flattening the inset there would strand the focused box under a real one. TextBlock.detail never wrapped, where .body beside it always has. A block that neither wraps nor trims lays out at its full single-line width and the parent clips it, with no ellipsis to say anything is missing — so the add sheet read "A machine to connect to, filed under a group if you" and stopped. Which of the fifty-odd .detail blocks overflows is a function of width and font scale rather than of the string, so this is a style rather than an attribute on the handful visibly wrong on one phone. The nine that genuinely want one line now say NoWrap beside their TextTrimming, which is the pairing that was always meant. The two port boxes are NumericUpDowns and were the only unthemed controls on this head — Fluent's border and fill, next to a TextBox.field sharing their row. App.axaml has styled NumericUpDown for the desktop all along; this head never had one. Three buttons classed "icon accent" were painting Fluent's own Button.accent, #6275AC, a blue in no palette here. The desktop overrides that style and this head did not, so the same class read correctly there and not here. The host key sheet said "First contact with 10.0.2.2 · 2222 . Nothing in this keychain…" — Avalonia puts a space between adjacent Runs and the last one began with the full stop. A gap before a full stop, on the one dialogue whose whole job is to be read carefully before it is answered. ── checks ────────────────────────────────────────────────────────────────────────────────────────────── manual-checks gains 10.4a and 11.10f. 10.4 already covered dismissing the keyboard with back or the chevron but not leaving the app with it up, which is the path the inset bug shipped through; 10.4a covers both directions, since a fix that flattened the inset on every resume would pass one half and fail the other. 11.10f guards the keypad against a ScrollViewer being put back around it. |
||
|
|
ebb88c8ae4 |
Give the phone both pickers, and settle who signs the APK
The files screen could browse a remote and delete on it, and that was all: there is no browsable local filesystem on Android for a second pane to show, so the gesture the desktop is built around — choose on the left, press the arrow — has nothing to stand on. What replaces it is the platform's own two pickers. ADD FILES is ACTION_OPEN_DOCUMENT, so a document is pointed at wherever it lives and goes to the directory showing; SAVE FILE is ACTION_CREATE_DOCUMENT for the selected row. Both stage through the application's cache, and that copy is a requirement rather than a shortcut. android-port.md predicted a picked document would be a third IRemoteFileStore beside SFTP and S3; it cannot be. FileTransferQueue seeks, because an upload resumes from the byte the last attempt reached, and a content:// URI has no path behind it, no length worth trusting, no promised seek and no grant that survives the document being edited underneath it. Copying first costs one class in the head and nothing at all in the shared layers, where the alternative was every resume rule rewritten around a stream that cannot rewind. The copy is deleted when the transfer completes, kept while it is stopped so RESUME still has something to read, and swept at the next launch — which is the one moment emptying that directory is provably safe, since nothing has queued anything yet. Coming out had a decision going in did not: when to ask where it goes. The save picker is raised before the transfer, so the download runs into the same staging directory and hands its bytes to a callback the head supplied, held against the transfer id so a RETRY still lands where the person pointed. Asking afterwards would put the picker minutes from the button that caused it and, on a phone, usually while the application is backgrounded and Android will not show one at all. The cost is that the picker creates its file when it is dismissed, so a download that then fails leaves an empty one there; that is said on the screen, in the README and in the manual checks rather than left to be discovered. A delivery that fails keeps the staged bytes for the sweep instead of throwing away the one copy of something just fetched over somebody's network. The foreground service counts transfers now, which is the half of it that matters most here: a shell survives backgrounding because somebody is looking at it, and an upload has to survive precisely when nobody is. Queued counts as active, so putting five files in and locking the phone moves five files. The seam was built for this and wired to () => 0 because nothing could fill the queue. Alongside it, ADR 0010 answers the second question android-port.md left open, and it had to be answered before the first release rather than at upload time: a new Play app must use App Bundles and therefore Play App Signing, and an installed app can only be updated by a package signed with the same key, so the first release picks an identity for good. The project holds the key, offline and never in CI — the workflow's package step now says so where somebody would break it — and a DodoSSH deployment never serves the client, because a download link on your own server hands the binary that holds the plaintext to the party the whole threat model is about. The README's M1 gap note was stale in both halves and is replaced by what is actually true: credentials have an editor and a REMEMBER tick, and the device key registers into the TPM under a CNG policy that makes the consent dialog a condition of using it. What is left is the floor rather than a gap — no TPM, or no Windows, means the passphrase on every launch. |
||
|
|
7a3a521c59 |
Give the phone the rest of its screens, and a way in
All seven screens of the design, plus the two it does not draw because it starts at an enrolled phone: naming a server, and choosing a passphrase. The five states docs/android-port.md worried about losing at 360dp are all here and none of them softened. The changed-key refusal is a full-screen panel rather than a bottom sheet, because a sheet is swipe-to-dismiss by convention and that screen must have no way forward. The recovery code raises FLAG_SECURE for its own state and lowers it afterwards, so the sentence about screenshots is true rather than decorative. The delete confirmations keep their counts and replace the row in place. Signing in works, and the seam it needed is worth more than the implementation: IAuthorizationCallback now sits between OidcClient and the loopback listener, so the two heads differ in where the response arrives and in nothing else. PKCE, the state check, discovery, the token exchange and the key binding stay one implementation — a second OIDC client would be a second place for a security bug to live. The phone registers a private-use scheme with the system rather than binding a loopback port, which on a shared device any other app can do first. The accessory key row needed TerminalWorkspace.SendInputAsync: ordinary typing goes from the renderer straight down the socket, and there was no way in for the keys a software keyboard does not have. Ctrl latches, because one thumb cannot chord, and the latch is drawn — a modifier that is on and does not look on is how somebody sends ^L to a database prompt believing they typed an l. 597 client tests green, including two new ones for the input path and one for the terminal surface command. Nothing has run on a device. |
||
|
|
fe9d7fc289 |
Give DodoSSH a phone, and a shared shell for both heads to drive
The Android head from docs/android-port.md, taken as far as its step 6. Step 3, the spike, is answered and its throwaway screen is gone: libsodium.so and libe_sqlite3.so are both in the arm64 APK, so NSec resolves its native half on Android despite shipping no Android build, and the local cache opens. Two findings the audit could not have had: Avalonia.Controls.WebView only ships net10.0-android36.0, which settles the open "which Android versions" question at targetSdk 36; and Android has blocked cleartext HTTP since API 28, so the terminal renderer needs a network security config scoped to 127.0.0.1 or the WebView loads nothing. DodoSSH.Client.Shell is new and is why the phone can exist: the view models, the terminal renderer files and the palette moved there so both heads drive one state machine and draw from one set of tokens. The desktop head is otherwise untouched and its 144 tests still pass. The platform pieces behind interfaces that already existed: the profile directory from filesDir, a device key wrapped by a StrongBox-backed key that a fingerprint releases, and a foreground service so a shell outliving a vault lock stays true on a platform that stops backgrounded processes. Sign-in is deliberately absent rather than approximated. It needs an app link, because reusing the desktop loopback listener is the attack RFC 8252 section 8.3 names. |