Public Access
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.
This commit is contained in:
@@ -19,17 +19,28 @@
|
||||
browsable local filesystem to put in the other half. TransfersViewModel's local pane — LocalPath,
|
||||
LocalRoots, LocalEntries — is desktop-only and is left alone here rather than shown empty.
|
||||
|
||||
◆ **And that is why neither DOWNLOAD nor UPLOAD is on this screen.** Both commands exist and both work;
|
||||
what they work *against* is the local pane. `QueueDownloads` writes to `Path.Combine(LocalPath, name)`,
|
||||
and `LocalPath` starts at `LocalDirectory.Home` — `SpecialFolder.UserProfile`, which on Android is the
|
||||
application's own private directory. A download would report success and put the file somewhere the
|
||||
person who asked for it cannot open it, which is worse than not offering it: a refusal is visible and a
|
||||
file in `/data/user/0/…` is not. The way in and out is the system document picker, which is the shape
|
||||
docs/android-port.md decided on and is the next piece of work.
|
||||
◆ **ADD FILES is the way in, and it is the system document picker rather than an UPLOAD button.** There
|
||||
is nothing local to select from, so the gesture cannot be "choose on the left, press the arrow": it is
|
||||
"point at a document wherever it lives, and it goes to the directory showing". What Android hands back
|
||||
is a `content://` URI, so `DocumentStaging` copies it into this application's cache and queues the copy —
|
||||
the queue needs a path, a length and a seek, and a document URI promises none of the three. See that
|
||||
class for why the copy is a requirement rather than a shortcut, and `QueueStagedUploads` for when it is
|
||||
deleted again.
|
||||
|
||||
So what ships is browsing a remote, and the two remote-side operations that need nothing local —
|
||||
opening a directory and deleting. The queue is drawn because a transfer can still be running when this
|
||||
screen is opened; it is simply not something this head can start yet.
|
||||
◆ **SAVE FILE is the way out, and it is the save picker rather than a DOWNLOAD button.** `QueueDownloads`
|
||||
writes to `Path.Combine(LocalPath, name)`, and `LocalPath` on Android is the application's own private
|
||||
directory — a download that way would report success and leave the file where the person who asked for
|
||||
it cannot open it. So this head does not use it: `QueueDeliveredDownload` runs the transfer into the
|
||||
cache and hands the finished bytes to the document `ACTION_CREATE_DOCUMENT` made.
|
||||
|
||||
The destination is chosen *before* the transfer, which is a decision with a visible cost — the picker
|
||||
creates the document when it is dismissed, so a download that then fails leaves an empty file where it
|
||||
was pointed. The alternative is a picker raised minutes later, over whatever the person moved on to and
|
||||
frequently while this application is backgrounded, where Android will not show one at all.
|
||||
|
||||
So what ships is browsing a remote and moving files both ways, plus the two remote-side operations that
|
||||
need nothing local — opening a directory and deleting. The queue is drawn under the actions, because
|
||||
this head can now fill it.
|
||||
|
||||
◆ **The host key prompts are here too.** File transfer is a second, separate authenticated connection
|
||||
and it makes its own trust decision — the host records a second login. So this screen carries its own
|
||||
@@ -255,6 +266,70 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
◆ The queue, above the actions and only when it has something in it. Bounded and scrolling rather
|
||||
than growing: five files queued must not push the buttons off the bottom of the screen, which on a
|
||||
phone is how a screen becomes unusable rather than merely tall.
|
||||
-->
|
||||
<ScrollViewer MaxHeight="164" IsVisible="{Binding HasTransfers}"
|
||||
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding Transfers}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TransferRowViewModel">
|
||||
<Grid ColumnDefinitions="14,*,Auto" Margin="0,3">
|
||||
<TextBlock Grid.Column="0" Classes="mono" FontSize="12" Text="{Binding Arrow}"
|
||||
VerticalAlignment="Center" Foreground="{StaticResource AccentText}" />
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="8,0" Spacing="3">
|
||||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<ProgressBar Height="3" Minimum="0" Maximum="100" Value="{Binding Percent}"
|
||||
Foreground="{StaticResource Accent}"
|
||||
Background="{StaticResource Raised}" />
|
||||
<TextBlock Classes="detail" Text="{Binding Progress}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
One button per row, never two: whichever of the three applies to the state it is in.
|
||||
A phone row has space for a name, a bar and one 44-pixel target, and the three are
|
||||
mutually exclusive by construction — IsRunning and CanRetry cannot both hold.
|
||||
-->
|
||||
<StackPanel Grid.Column="2" VerticalAlignment="Center">
|
||||
<Button Classes="secondary" MinHeight="36" Padding="10,0" Content="STOP"
|
||||
IsVisible="{Binding IsRunning}"
|
||||
Command="{Binding $parent[views:FilesScreen].((vm:TransfersViewModel)DataContext).CancelTransferCommand}"
|
||||
CommandParameter="{Binding}" />
|
||||
<Button Classes="secondary" MinHeight="36" Padding="10,0" Content="{Binding RetryLabel}"
|
||||
IsVisible="{Binding CanRetry}"
|
||||
Command="{Binding $parent[views:FilesScreen].((vm:TransfersViewModel)DataContext).RetryTransferCommand}"
|
||||
CommandParameter="{Binding}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<Button Classes="secondary" Height="40" Content="CLEAR FINISHED" HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding HasTransfers}" Command="{Binding ClearCompletedCommand}" />
|
||||
|
||||
<!--
|
||||
◆ The two directions, on their own row above the pair below — because DELETE is the button on this
|
||||
screen that nothing can undo, and it must not sit at a thumb's width from the ones somebody presses
|
||||
often. ADD FILES is the primary of the two: it is the one that needs no selection, and the one this
|
||||
screen exists for on a phone.
|
||||
|
||||
SAVE FILE takes the selected row rather than several, and that asymmetry is the platform's: the
|
||||
save picker names one destination. CanDownload is the desktop's own flag for the same question —
|
||||
a file is selected and something is connected — and is reused rather than restated here.
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding !IsConfirmingRemoteDeletion}">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="ADD FILES" Click="OnAddFiles" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="SAVE FILE" Click="OnSaveFile"
|
||||
IsEnabled="{Binding CanDownload}" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding !IsConfirmingRemoteDeletion}">
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="DELETE"
|
||||
Command="{Binding DeleteRemoteCommand}" IsEnabled="{Binding CanDeleteRemote}" />
|
||||
@@ -263,18 +338,11 @@
|
||||
</Grid>
|
||||
|
||||
<TextBlock Classes="body" IsVisible="{Binding !IsConfirmingRemoteDeletion}"
|
||||
Text="Copying files to and from this phone needs the system document picker, which is not built yet — see the note at the top of this screen. Browsing, opening and deleting work." />
|
||||
Text="ADD FILES picks documents to send. SAVE FILE asks where the selected file should be kept — the file is created there when you choose it, so a transfer that fails leaves it empty." />
|
||||
|
||||
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}" TextWrapping="Wrap"
|
||||
Text="{Binding Status}" />
|
||||
|
||||
<!--
|
||||
There is no queue on this screen, and that follows from the note at the top rather than being a
|
||||
separate decision: nothing here can enqueue a transfer, so a queue would be a region that is
|
||||
empty for every possible state of the application. It comes back with the document picker, along
|
||||
with the two buttons that would fill it.
|
||||
-->
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user