185790fb14d45273282753a630f4a4cf131a1681
59
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a0d53b0c9d |
Give the phone its screens back by not stealing a data context
Signing in on the phone reached an unlocked shell showing PREFERENCES over the middle of the
screen, with a bottom bar whose four buttons did nothing. The buttons were fine. Every one
of them changed the shell's screen exactly as asked, and nothing moved, because an opaque
panel was sitting on top of the whole page area and never came down.
PendingScreen set DataContext = this in its constructor, so that Heading and Detail could be
written as plain bindings inside its own XAML. That is not a private arrangement: a binding
the parent writes ON one of these — IsVisible="{Binding IsPreferencesShowing}" in
PhoneShell — resolves against this control's data context, which was no longer the shell.
The binding looked for a shell property on a PendingScreen, found nothing, and left
IsVisible at its default. Its default is true. So the panel that says "this screen is not
built yet" was permanently visible, last in the Panel and therefore on top of the host list
and the keychain both — and the terminal underneath them.
The two properties are read with $parent now and the control inherits its context like every
other screen. There is no x:DataType on it any more either, so a plain binding here is a
compile error rather than a silently missing screen.
The desktop head has the other half of this lesson written down already: MainWindow gives the
terminal's IsVisible a FallbackValue precisely because an unresolved visibility binding does
not hide anything, it shows everything. That note was about the previewer. This is what it
looks like at runtime.
Verified by reproducing the mechanism rather than by reasoning about it: a control that owns
its data context ignores a parent's IsVisible binding and stays visible; one that inherits
obeys it. The head builds in Debug and Release. Nothing has been run on a device, as ever —
see docs/android-port.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1db8bed872 |
Let a cancelled sign-in end the sign-in rather than the timeout
Backing out of the login page on Android left the shell showing "Opening your browser to sign in…" with the button disabled for five minutes. Nothing was wrong except that nobody told it: the redirect callback only ever completed when an intent arrived, so a user who pressed back was waiting on OidcClient's browser timeout to expire before the flow failed and the button came back. There is no cancel event to subscribe to on this platform. Pressing back, dismissing the browser and closing a provider's error page are indistinguishable from here — the browser goes away and this application is foreground again with nothing delivered — so being resumed while a sign-in is still waiting is the signal, and the only one there is. The launcher records that a browser took the intent, OnResume fails the wait, and the guard means the resumes that have nothing to do with signing in (a launch, recents, the keystore's fingerprint prompt) go through untouched. An exception rather than a cancellation, because OidcClient reads a cancelled wait as its own timeout expiring and would report five minutes passing to somebody who waited two seconds. It cannot steal a successful sign-in either: Android delivers the redirect to OnNewIntent before resuming the activity, so the completion is already settled and the attempt does nothing. The enrollment key-binding trip through the browser is covered by the same change, since it waits on the same callback. The OnNewIntent remark had been sitting above OnResume, describing a method two below it. Moved back, since the new remark wanted the space and the old one was wrong where it was. Verified by building the head in Debug and Release. The behaviour itself is unverified for the reason docs/android-port.md gives about this whole head: nothing has been run on a device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
57d4b30557 |
Put the app's own mark on the launcher
The sign-in and locked screens both draw the same thing — a square outline in the accent with >_ inside it — and the launcher was still showing the stock Android silhouette, so the icon somebody taps and the icon the app opens onto had nothing to do with each other. Redrawn as a vector rather than exported from the screen as a bitmap. There is one geometry here and no set of density buckets to update four of and forget the fifth, and the accent stays a number that can be diffed against Palette.axaml rather than a colour baked into a PNG. The hex is written out because an Android resource cannot reference a XAML dictionary — the same duplication colors.xml already carries for the window background, with the same obligation attached. Adaptive only, no raster fallback. Adaptive icons landed in API 26 and this head requires 28, so there is no device it ships to that would need the bitmaps; density buckets exist to choose between PNGs and there is nothing to choose. The background layer is the same @color/dodo_window as the window and the status bar, so the mark sits on the app's own near-black rather than on a second one almost like it. Two departures from the screen, both because a launcher is looked at much smaller than a sign-in header. The box is 42 across rather than the 48 that first suggested itself: 72 of the 108 survives masking, but that is a width, and a square meets a circular mask at its corners — at 48 they land 33.9 out against a radius of 36 and read as clipped despite technically clearing it. And the strokes are 2.2 and 2.8 where proportional fidelity to a 1px border on 44px would be 1.0, which a launcher drawing this at 48dp would render as half a pixel of nothing. A monochrome layer too, for the Android 13+ themed-icon setting. Without one a launcher with themed icons on falls back to the full-colour icon, which would leave this the single green thing on an otherwise recoloured home screen. Verified in the packaged APK: the icon resolves at all five densities, the three layers resolve, and the compiled vector carries the geometry above. The launcher rendering itself was checked against local renders under circular and squircle masks at 144 and 64 px, not on the device — the phone was locked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
215e73b07f |
Let the phone's theme past the activity it is attached to
The head has now run on a device, and the first thing it did was die on the way up. DodoTheme parented @android:style/Theme.Material.NoActionBar, but AvaloniaMainActivity descends from AndroidX's AppCompatActivity, which asserts its own theme attributes while inflating and throws — "You need to use a Theme.AppCompat theme (or descendant)" — before a single Avalonia frame exists. The platform's own parents are the ones that look right, which is why the audit read as correct and the launcher icon still opened onto a splash screen and then nothing. Theme.AppCompat.NoActionBar instead, dark rather than .Light because every override below it repaints the window near-black regardless. The no-action-bar and status-bar decisions those overrides carry are untouched, so the reason they are there — a header that has to hold the vault name, and a clock that would otherwise be dark-on-dark — still holds. Verified on a OnePlus CPH2765: builds, deploys, and reaches the sign-in screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4300d917a8 |
Stop making people wait for a handshake, and give the host list a pointer
Connecting held the vault's busy gate, which meant a window that did nothing visible for as long as a machine took to answer — and against one that is merely asleep, that is the whole timeout. The gate is gone from that one command. A tab now appears in the strip in the same turn as the click, carrying "connecting…" rather than a pane, and the terminal's rectangle draws a card naming the host and the address being dialled. Every other screen stays usable, and two connections can be in flight at once. That splits the vault's one connection event into three, carrying an attempt id, because "which tab is this about" can no longer be answered by "the most recent one". The id also buys the two kinds of not-connecting their different endings: a refusal stays in the strip as a tab holding its reason, since by then the user is quite likely three screens away and a status line they are not looking at is not where a failure should end; a host key question takes the tab away and puts the window back on HOSTS, because the prompt is drawn there and a tab claiming failure would be competing with the thing about to resume it. ConnectAsync takes no CancellationToken any more, and that is load-bearing rather than tidying. A [RelayCommand] over a method that takes one generates a command that cancels the previous execution's token on every invocation — so asking for a second machine silently abandoned the first, measured as the first tab disappearing with "Cancelled." the instant the second was asked for. Giving up on a connection is closing its tab, and a session that lands after that is adopted rather than dropped: a shell running with nothing naming it cannot be closed at all. A tab is marked active on IsShowing rather than IsSelected. The selection survives navigating away — that is what makes the strip a way back to a terminal instead of a way to lose one — so a tab lit while preferences filled the window was a second "you are here" mark pointing at something nobody could see. The nav rail's own entries have always made this distinction. The host list grows the two gestures it looked like it already had. A right click selects the row under the pointer before opening a menu of Connect, Edit and Delete — the menu is on the list rather than in the item template, so its entries are the vault's own commands and not a row's, and it is cancelled outright over a group heading. Dragging a host onto a heading files it there, onto a host files it beside that one, and onto UNGROUPED takes it out of a group; the write is one field of one host through the same repository a save uses, refused while the editor is open because a drop is a gesture on the list and not on a half-typed form. Clicking a result in the palette connects, which is what a list of hosts under a search box looks like it does. It went through the shell's own command, so the pointer and Enter take one path. And the files screen's two pickers followed the vault's lists once, at unlock: a host or a bucket created afterwards could not be picked until the keychain had been locked and opened again, with nothing on screen explaining why the machine plainly in the host list was missing. They follow the collections now, re-finding the selection by id across the rebuild a sync pass causes every minute. 165 shell tests and 69 layout tests green, including the connecting tab, both failure endings, two connections at once, a connection in flight across a lock, and the right click acting on the row under the pointer rather than on the selection. The drag itself is in docs/manual-checks.md with the rest of phase 7 — headless Avalonia has no platform drag, and a test that claimed to have dropped something would pass while confirming nothing. |
||
|
|
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. |
||
|
|
81e7e6d939 |
Write down what the phone found, and stop it rotting
docs/android-port.md was an audit of work not started; it now says what is built. Three of its statements needed correcting rather than extending, and they are marked where they sit: the Android version question is settled and was never as open as it looked, because Avalonia.Controls.WebView ships only a net10.0-android36.0 assembly and nothing lower can resolve it; cleartext to loopback has to be permitted explicitly, which the audit missed entirely; and the spike produced a structural change it did not anticipate, in DodoSSH.Client.Shell. A CI job of its own, because the head is deliberately not in DodoSSH.slnx and a project outside the solution is a project nobody notices breaking. It packages as well as builds: a native library with no Android ABI and an assembly that will not dex are both invisible to a compile, and both are exactly what this head is exposed to. The README says plainly that signing in is not built, that a fingerprint re-enrolment destroys the device key, that a notification appears while a shell is open, and that none of it has run on a device. |
||
|
|
2caedd93ff |
Merge branch 'main' into the Android head
Main grew the screens the host-management plan called for — hosts, pins, snippets, logs, import, teams — plus the ObjectStore and Import projects behind two of them, and moved WindowsDeviceKeyStore into the desktop head's Platform folder. Five of those view models landed in a directory this branch had already moved, so they join the rest in DodoSSH.Client.Shell: git spotted the rename and put them there, and the namespaces followed. Shell picks up ObjectStore and Import as a result, which the Android head then gets transitively and will use neither of at first — scoped storage means there is no ~/.ssh/config to import, and file transfer is out of its first scope. Desktop suites green at 155 and 64. |
||
|
|
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. |