Public Access
Let the phone replace itself, and give CI a channel it may sign
The Android head had no updater and no release path, and the two are one problem: Android refuses an update signed by a different key, and CI generates a fresh debug key in every container. An APK released from a workflow could be installed once and never updated again — each new one an uninstall, which on this product means losing the cache, the outbox and the device key. So there are two channels, and they are two applications because the platform gives no third option. dev.dodotech.dodossh is cut from a v* tag by a person running scripts/release-android.ps1 with the key ADR 0011 rule 1 keeps off runners. dev.dodotech.dodossh.nightly is cut from main by CI and signed with a keystore committed here in the open — a key everybody has cannot be stolen and grants nothing by being held, which is why putting it in CI does not touch the rule. Neither can update the other, by construction. See ADR 0014. The android job assumed an image with a JDK and an Android SDK on it, which is what a GitHub runner is and what this project's is not. It now installs a JDK, fetches Google's command-line tools, accepts the licences and installs API 36 — each a no-op where it is already satisfied, and each cached by the persistent runner's own disk rather than by an action that would move a quarter of a gigabyte to rebuild a directory that never left. The client reads a small JSON manifest beside the APK, the counterpart of releases.win.json, and compares Android's versionCode rather than a version name: that integer is what the platform itself uses to accept or refuse an install, so comparing anything else would offer updates the phone then rejects. It fetches, and then asks Android to ask — the system draws its own confirmation, and from API 26 will not draw even that until unknown sources is on for this application. IUpdateChannel gained ApplyingEndsTheProcess. On Windows applying replaces the files and restarts, so the shell disposes the vault first and that is what zeroes the keys. On the phone the install is a request and the answer may be no, so disposing first would answer "not now" with a locked keychain and every shell closed — a punishment for declining an update. Two measured bugs found on the way, both older than this work and both invisible to a -getProperty check. ApplicationDisplayVersion is read by the Android targets in a top-level PropertyGroup, so the target setting it from MinVer ran after the only thing that reads it: every APK ever built here said versionName 1.0.0. And nothing found so far varies the launcher name per channel — four mechanisms tried, all of them recorded in platform-flags, none of them reaching the label the launcher shows. The two channels share an icon name for now and are told apart by package name, version, and what the preferences screen says.
This commit is contained in:
@@ -70,11 +70,32 @@ public interface IUpdateChannel
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task DownloadAsync(AvailableUpdate update, IProgress<int> progress, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="ApplyAndRestart"/> ends this process itself.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// True on a desktop, where applying an update is this application replacing its own files and
|
||||
/// restarting — the caller tears the vault down first, because that is what zeroes the identity keys,
|
||||
/// the vault keys and the cache key while there is still a process to zero them in.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>False on the phone, and it is not a detail.</b> There, applying hands the package to Android's
|
||||
/// own installer and returns; what happens next is a system dialogue the user may perfectly well say
|
||||
/// no to. A caller that had already disposed the vault would answer "not now" with a locked keychain
|
||||
/// and every shell gone — punishing somebody for declining an update. Where this is false the caller
|
||||
/// leaves the session alone, and the keys go when the platform kills the process, which it does the
|
||||
/// moment the install is agreed to.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
bool ApplyingEndsTheProcess { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Swaps this build for the downloaded one and starts it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>This does not return.</b> It is the reason the whole capability is behind an interface rather
|
||||
/// <b>This does not return where <see cref="ApplyingEndsTheProcess"/> is true.</b> It is the reason the
|
||||
/// whole capability is behind an interface rather
|
||||
/// than being three lines inline in the view model: a test cannot drive a method that ends the process,
|
||||
/// and the single most important thing to be able to assert about this feature is that a downloaded
|
||||
/// update is <em>never</em> applied unless somebody asked for it. A fake that records the call instead
|
||||
@@ -109,6 +130,14 @@ public sealed class UnavailableUpdateChannel : IUpdateChannel
|
||||
/// <inheritdoc />
|
||||
public string CurrentVersion => ClientVersion.Current;
|
||||
|
||||
/// <summary>Always true, and never read: nothing here applies anything.</summary>
|
||||
/// <remarks>
|
||||
/// True rather than false because it is the answer that makes a caller who ignores
|
||||
/// <see cref="IsSupported"/> do the safer thing — tear the session down — before reaching a method
|
||||
/// that is about to throw at it anyway.
|
||||
/// </remarks>
|
||||
public bool ApplyingEndsTheProcess => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<AvailableUpdate?> CheckAsync(CancellationToken cancellationToken) =>
|
||||
Task.FromResult<AvailableUpdate?>(null);
|
||||
|
||||
Reference in New Issue
Block a user