Public Access
Let the recovery code be copied, and give the phone a clipboard to copy to
Both screens had made the code selectable and both said why: a person who cannot get it out of the box photographs the screen, and a screenshot is a worse home for it than a clipboard. This finishes that argument. Selecting 64 characters of letter-spaced monospace with a thumb is the version of "possible" people give up on halfway — and on the phone the screen blocks screenshots, so the honest remaining options were retyping it or losing it. It is the one secret this application deliberately offers to a clipboard, and the contrast with the keychain's copy is the whole argument rather than an inconsistency. There, copying the private half is refused outright, because installing a key means pasting the public one and the private one has no business leaving the vault. Here there is no better route: the code exists for one screen, is stored nowhere, and has to reach a password manager. The clipboard is the intended destination rather than a way round the design. The sentence afterwards matters as much as the copy, and is asserted: a clipboard is a staging post, this screen is the only place the code exists, and the next thing copied replaces it. Somebody who copies and does nothing has not saved it. The phone had no clipboard delegate at all — the desktop passed one and this head passed null — so COPY PUBLIC KEY on the keychain answered "this machine has no clipboard" on a device that plainly has one. Nothing about that was platform shaped: Android has a clipboard and Avalonia surfaces it through the same TopLevel. Wiring it fixes that copy too. The test fixture built its shell without a clipboard, which modelled the bug rather than the product, so it has one now and the public-key test asserts what lands there instead of the refusal. The refusal keeps its own test, on a shell built without one, because the view model reads the delegate's absence rather than an empty result — and because a button that silently does nothing on this screen is worse than one that refuses.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
using DodoSSH.Client.Android.Platform;
|
||||
@@ -79,7 +81,14 @@ public sealed partial class DodoSshApp : Avalonia.Application
|
||||
// RESUME has something to read — and whatever a process death interrupted. See DocumentStaging.
|
||||
DocumentStaging.Sweep();
|
||||
|
||||
var viewModel = ComposeShell(paths, caches, workspace, knownHosts, connections);
|
||||
// Built before the view model, because the clipboard is reached through it — see ClipboardWriter,
|
||||
// which takes the surface and looks the TopLevel up on each call rather than now.
|
||||
var shell = new PhoneShell();
|
||||
|
||||
var viewModel = ComposeShell(
|
||||
paths, caches, workspace, knownHosts, connections, ClipboardWriter(shell));
|
||||
|
||||
shell.DataContext = viewModel;
|
||||
|
||||
// Difference 2: the foreground service, which is what makes TerminalWorkspace's promise — that a
|
||||
// shell outlives a vault lock — true on a platform that stops backgrounded processes.
|
||||
@@ -104,9 +113,40 @@ public sealed partial class DodoSshApp : Avalonia.Application
|
||||
|
||||
keepAlive.Refresh();
|
||||
|
||||
return new PhoneShell { DataContext = viewModel };
|
||||
return shell;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writing to this phone's clipboard.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// ◆ <b>This head had none, and every control that wanted one said so out loud.</b> COPY PUBLIC KEY on
|
||||
/// the keychain answered "This machine has no clipboard" on a device that plainly has one, because the
|
||||
/// delegate was simply never wired here — the desktop passed one and the phone passed null. Android has
|
||||
/// a clipboard and Avalonia surfaces it through the same <c>TopLevel</c> the desktop reaches, so there
|
||||
/// was nothing platform-shaped about the gap.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Looked up per call rather than captured, exactly as the desktop's is: at composition there is no
|
||||
/// <c>TopLevel</c> yet, because this is the method building the view it will be attached to. A machine
|
||||
/// that somehow has none falls through silently and the view model decides what to say, which is what
|
||||
/// keeps "no clipboard here" and "copied" different answers.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A delegate rather than an <c>IClipboard</c>, so nothing in the view models needs a visual and every
|
||||
/// test that drives them stays window-free.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private static Func<string, Task> ClipboardWriter(Visual surface) =>
|
||||
async text =>
|
||||
{
|
||||
if (TopLevel.GetTopLevel(surface) is { Clipboard: { } clipboard })
|
||||
{
|
||||
await clipboard.SetTextAsync(text).ConfigureAwait(false);
|
||||
}
|
||||
};
|
||||
|
||||
/// <remarks>
|
||||
/// Split from <see cref="Compose"/> only for length. The division is a real one though: above this is
|
||||
/// the platform graph, and below it is the shell every head shares.
|
||||
@@ -116,7 +156,8 @@ public sealed partial class DodoSshApp : Avalonia.Application
|
||||
ClientCacheFactory caches,
|
||||
TerminalWorkspace workspace,
|
||||
VaultKnownHostStore knownHosts,
|
||||
SshNetConnectionFactory connections)
|
||||
SshNetConnectionFactory connections,
|
||||
Func<string, Task> copyToClipboard)
|
||||
{
|
||||
// Difference 3: the Android keystore, with a fingerprint or the device credential releasing the
|
||||
// key. A straight implementation of the interface the session layer has always taken.
|
||||
@@ -165,6 +206,8 @@ public sealed partial class DodoSshApp : Avalonia.Application
|
||||
// would name the same machine. See PhoneEnvironment.DeviceName.
|
||||
deviceName: PhoneEnvironment.DeviceName,
|
||||
|
||||
copyToClipboard: copyToClipboard,
|
||||
|
||||
updates: updates);
|
||||
|
||||
// Started rather than awaited: framework initialisation must not block on a schema migration. The
|
||||
|
||||
Reference in New Issue
Block a user