GeneralUpdate.Avalonia.Android — Execution Flow Deep Dive
Target Audience: Developers integrating auto-update into Avalonia Android apps
After reading you will understand:
- How
ValidateAsyncqueries the server and picks the newest APK (the caller only supplies the current version)- How to choose between
PrepareUpdateAsyncand the phased three-step APIHttpResumableApkDownloaderinternals: HEAD probe + Range requests + sidecar metadata + atomic rename- Dual verification of file size and SHA256
- Android APK installation via the FileProvider URI authorization flow
- The installation result loop: why "the installer launched" is not "the installation succeeded"
SemaphoreSlim(1,1)operation gate andDisposelifetime contractsIUpdateEventDispatcherUI thread dispatch mechanism- Multi-protocol authentication architecture (HMAC / Bearer / API Key / Basic) and where credentials are sent
Table of Contents
- Architecture overview
- Entry point: the GeneralUpdateBootstrap factory
- Two calling styles: one-shot and phased
- ValidateAsync — server query and version validation
- DownloadAndVerifyAsync — download and verification
- Resumable download: HttpResumableApkDownloader internals
- Hash and file size verification
- LaunchInstallerAsync — APK installation handoff
- The installation result loop
- Concurrency and threading model
- Event dispatching and the UI thread
- Multi-protocol authentication
- Key code path index
1. Architecture Overview
1.1 Full abstraction plus factory assembly
Avalonia.Android uses interface abstractions assembled by a factory — every core capability has an interface that can be replaced independently:
┌──────────────────────────────────────────────────────────────────────┐
│ GeneralUpdateBootstrap (static factory) │
│ CreateDefault(options, ...) → IAndroidBootstrap │
├──────────────────────────────────────────────────────────────────────┤
│ AndroidBootstrap (orchestration) │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────┐ │
│ │ IVersion │ │ IUpdatePackage │ │ IUpdateDownloader │ │
│ │ Comparer │ │ Source │ │ Resumable HTTP │ │
│ │ Version rules │ │ Server query │ │ download │ │
│ └────────────────┘ └────────────────┘ └────────────────────┘ │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────┐ │
│ │ IHashValidator │ │ IApkInstaller │ │ IFileStorage │ │
│ │ SHA256 │ │ System │ │ File system │ │
│ │ │ │ installer │ │ │ │
│ └────────────────┘ └────────────────┘ └────────────────────┘ │
│ │
│ ┌────────────────────┐ ┌────────────────────┐ ┌───────────────┐ │
│ │ IInstallationStore │ │ IUpdateEvent │ │ IUpdateLogger │ │
│ │ Installation │ │ Dispatcher │ │ Logging │ │
│ │ record persistence │ │ UI thread dispatch │ │ │ │
│ └────────────────────┘ └────────────────────┘ └───────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ HttpDownloadOptions (cross-cutting) │ │
│ │ SSL policy / proxy / timeout / retry / auth │ │
│ └──────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
The default implementation behind each abstraction is listed in the component reference.
1.2 Two calling styles
| Style | Method | When to use |
|---|---|---|
| One-shot | PrepareUpdateAsync(currentVersion, ct) | The host only needs "a verified APK ready to install"; query, comparison, pre-check, download and verification run under one operation lock |
| Phased | ValidateAsync → DownloadAndVerifyAsync → LaunchInstallerAsync | You need to show a prompt after the check, a progress bar during download, and ask for confirmation before installing |
| Step | Method | Responsibility | Caller control point |
|---|---|---|---|
| 0 | ValidateAsync(currentVersion, ct) | Query the server + compare versions + pre-check | Decide whether to continue |
| 1 | DownloadAndVerifyAsync(packageInfo, ct) | Download + size check + SHA256 check | Show progress UI |
| 2 | LaunchInstallerAsync(packageInfo, path, ct) | Persist the installation record + launch the system installer | Call after user confirmation |
| — | CheckInstallationAsync(currentVersion, ct) | Offline reconciliation of the previous install | Call at startup / on return |
Since v0.0.1-beta.10, ValidateAsync no longer accepts an UpdatePackageInfo: package metadata is discovered by
the component from AndroidUpdateOptions.UpdateServer (or an injected IUpdatePackageSource). The caller only supplies
the version installed on the device, then passes UpdateCheckResult.PackageInfo to the download and install methods.
2. Entry Point: the GeneralUpdateBootstrap Factory
public static class GeneralUpdateBootstrap
{
public static IAndroidBootstrap CreateDefault(
AndroidUpdateOptions options,
IAndroidContextProvider? contextProvider = null, // DefaultAndroidContextProvider
IAndroidActivityProvider? activityProvider = null, // NullAndroidActivityProvider
HttpClient? httpClient = null, // borrowed, host-owned
IVersionComparer? versionComparer = null, // SystemVersionComparer
IUpdateEventDispatcher? eventDispatcher = null, // ImmediateEventDispatcher
IUpdateLogger? logger = null, // NoOpUpdateLogger
HttpDownloadOptions? httpOptions = null, // SSL / proxy / timeout / retry / auth
IUpdatePackageSource? packageSource = null, // HttpUpdatePackageClient
IInstallationStore? installationStore = null) // JsonFileInstallationStore
{
// 1. Resolve the download directory: options.DownloadDirectoryPath → <CacheDir>/update → <TempPath>/update
// 2. Resolve the installation record path: options.InstallationStateFilePath → <FilesDir>/update/installation.json
// 3. Create one shared HttpClient (a supplied client is borrowed and never reconfigured)
// 4. Assemble downloader / packageSource / hashValidator / installer / store and return AndroidBootstrap
}
}
Two side effects are worth noting:
- The download directory and installation record path are written back into
options(through awithcopy) so the downloader and installer share the same resolved paths DownloadBufferSize <= 0throwsArgumentOutOfRangeExceptionimmediately; when the Android context is unavailable andInstallationStateFilePathwas not set explicitly, anInvalidOperationExceptionis thrown
3. Two Calling Styles: One-Shot and Phased
3.1 One-shot: PrepareUpdateAsync
PrepareUpdateAsync never opens the installer or a permission page: it only brings the APK to a "verified, ready to install"
state. When there is no update or the pre-check skips it, Success = true, IsReadyToInstall = false and UpdateFound = false.
3.2 Phased: the explicit three-step API
3.3 Lifecycle and state machine
State is updated by SetState() under a lock and can be read at any time with GetSnapshot():
public UpdateStateSnapshot GetSnapshot()
{
lock (_sync)
{
return _snapshot; // { State, FailureReason, Message }
}
}
4. ValidateAsync — Server Query and Version Validation
4.1 Full flow
4.2 Server query protocols
IUpdatePackageSource is the query entry point; the default HttpUpdatePackageClient supports two modes:
| Mode | Trigger | Behavior |
|---|---|---|
| GeneralUpdate verification protocol | UpdateServer.UseJsonEndpoint = false (default) | POST body version / appKey / appType / platform / productId, response {"code":200,"body":[...]} |
| Static JSON | UpdateServer.UseJsonEndpoint = true | GET a single UpdatePackageInfo; HTTP 204 or null means no update |
Package filtering under the default protocol:
for each entry in body
├── entry.IsFreeze == true → skip (frozen packages are not pushed)
├── entry.PackageType ∉ {null, 0, 2} → skip (not a full package)
├── entry.Format is not apk/.apk → skip (URL must end with .apk when omitted)
├── ValidateMetadata() fails → throw InvalidDataException
└── otherwise → compare with the current latest and keep the higher version
ValidateMetadata() requires a non-empty Version, an absolute HTTP(S) DownloadUrl, FileSize >= 0 and a 64-character
hexadecimal Sha256.
4.3 Failure classification
| Exception | Reported result |
|---|---|
HttpRequestException / OperationCanceledException / IOException / TimeoutException | FailureReason = NetworkError |
JsonException / InvalidDataException / ArgumentException | FailureReason = InvalidMetadata |
No UpdateServer and no injected package source | InvalidDataException("Configure UpdateServer or supply an IUpdatePackageSource.") → InvalidMetadata |
| Caller cancellation during the request | State = Canceled, FailureReason = Canceled |
| Caller cancellation while waiting for the operation lock | OperationCanceledException is thrown (no result object) |
Transport, protocol and metadata errors are reported only through UpdateCheckResult.Success = false, FailureReason and
AddListenerUpdateFailed. The pre-check callback is not invoked, so business logic cannot mistake a failed query for
"an update is available".
4.4 Version comparer
public interface IVersionComparer
{
bool TryCompare(string currentVersion, string targetVersion,
out int compareResult, out string? errorMessage);
// compareResult > 0: targetVersion is newer (update available)
// compareResult = 0: equal
// compareResult < 0: targetVersion is older (downgrade)
}
The default SystemVersionComparer uses System.Version for semantic comparison. It is also reused while picking the
newest package from the response body.
5. DownloadAndVerifyAsync — Download and Verification
DownloadAndVerifyAsync takes packageInfo as a parameter, so it can be used with the result of ValidateAsync or called
standalone when the package metadata comes from another business channel.
6. Resumable Download: HttpResumableApkDownloader Internals
6.1 Mechanics
Download pipeline
│
├── Phase 0: upfront metadata validation
│ DownloadUrl must be absolute HTTP(S) and Sha256 non-empty, else → InvalidMetadata
│ Non-HTTPS without AllowInsecureHttpDownloads → InvalidMetadata
│
├── Phase 1: HEAD probe
│ Accept-Ranges / ETag / Content-Length / Last-Modified
│ HEAD returning 405/501 falls back to a GET probe
│
├── Phase 2: detect a partial download
│ {filename}.part + {filename}.json (sidecar)
│
├── Phase 3: resume consistency check
│ file name / ETag / LastModified mismatch → discard the temp file and restart
│ invalid sidecar JSON → discard and restart
│
├── Phase 4: GET with Range
│ fresh download: GET {url}
│ resume: GET {url} + Range: bytes={existingLength}-
│ server answers 200 OK (no Range support) → discard and restart
│ stream into {filename}.part and report progress continuously
│
├── Phase 5: atomic rename
│ {filename}.part → final file name (overwrite)
│ delete the sidecar
│
└── Progress reporting
downloaded bytes, total size, speed (bytes/s), percentage, status (Downloading/Resuming/Download completed)
6.2 Sidecar metadata
Persisted fields of DownloadResumeMetadata (the downloaded byte count is no longer stored — the length comes from the
.part file itself):
{
"downloadUrl": "https://cdn.example.com/app-v2.0.apk",
"expectedSha256": "a1b2c3d4...",
"expectedFileSize": 52428800,
"fileName": "app-v2.0.apk",
"etag": "\"abc123\"",
"lastModified": "2026-06-01T12:00:00Z"
}
6.3 Speed measurement
Speed measurement is inlined in the downloader (sliding window whose length is controlled by
AndroidUpdateOptions.SpeedSmoothingWindowSeconds, default 4 seconds) and is surfaced as
DownloadProgressInfo.DownloadSpeedBytesPerSecond.
6.4 Retry policy
| Situation | Retried |
|---|---|
| Transient network failures on HEAD / GET / response stream (timeout, 429, 5xx, no status code) | Yes, with resume |
| Metadata query failures | No |
| Permanent HTTP errors (for example 401) | No |
| Caller cancellation | No |
MaxRetryAttempts defaults to 3, meaning "1 initial attempt + 2 retries"; backoff is
min(30s, RetryBaseDelay * 2^attempt).
7. Hash and File Size Verification
7.1 Dual verification
DownloadAndVerifyAsync verification order:
1. FilePath not empty → otherwise FileIoError
2. File size check (when FileSize > 0)
→ mismatch → delete the file, return FileIoError
3. SHA256 check
→ mismatch → delete the file, return HashMismatch
→ cancellation → return Canceled (keep the file so it can be resumed)
7.2 Sha256HashValidator
public sealed class Sha256HashValidator : IHashValidator
{
public Sha256HashValidator(UpdateLanguage language = UpdateLanguage.English);
public async Task<HashValidationResult> ValidateSha256Async(
string filePath, string expectedSha256, CancellationToken ct = default)
{
// 1. empty expectedSha256 → Success = false
// 2. file not found → Success = false
// 3. compute the actual SHA256 and compare OrdinalIgnoreCase
// → mismatch: FailureReason = HashMismatch, ActualSha256 / ExpectedSha256 filled in
}
}
8. LaunchInstallerAsync — APK Installation Handoff
8.1 Android installation flow
The installation record is written atomically before the installer is launched, because Android may kill the current
process as soon as installation starts. Launching first and recording afterwards would make the target version unknowable
after a restart. If the write fails, the component returns FileIoError and does not launch the installer.
8.2 FileProvider configuration
In AndroidManifest.xml (authorities must equal AndroidUpdateOptions.FileProviderAuthority):
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.app.generalupdate.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/generalupdate_file_paths" />
</provider>
In Resources/xml/generalupdate_file_paths.xml, the paths must cover DownloadDirectoryPath (default <CacheDir>/update):
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="update_cache" path="update/" />
<files-path name="update_files" path="update/" />
</paths>
9. The Installation Result Loop
9.1 Why it is needed
LaunchInstallerAsync returning Success = true only means the installer intent was launched. The user may cancel,
the installation may fail, or the process may be killed after a successful install. To know whether the update actually took
effect, the host must reconcile the persisted target version with the version read from PackageManager on the next
launch or on return from the installer.
9.2 States and handling
| Result | Meaning | Suggested handling |
|---|---|---|
Success && State == None | No installation record | No prompt; continue with the normal update check |
HasPendingInstallation | The installed version is still below the target | Re-check and retry; never reset the record automatically |
IsInstalled | The device reached or exceeded the target | Report success; the confirmation is persisted and will not be raised again |
!Success | Invalid device version, or record read/write failure | Reported through AddListenerUpdateFailed; clear it with ResetInstallationAsync() after user confirmation |
Reconciliation does not touch the network. AddListenerInstallationConfirmed is raised only on the first durable
confirmation; repeated checks or process restarts do not re-raise it, and it is not a reliable message queue — UI state
should be restored from the returned InstallationCheckResult.
9.3 Record storage
The default JsonFileInstallationStore writes <FilesDir>/update/installation.json:
- Writes use a temporary file plus an atomic
File.Move(overwrite: true), followed by a flush to disk - The path can be overridden with
AndroidUpdateOptions.InstallationStateFilePath; do not place it in the download cache, which Android may clear - The record contains only
TargetVersion/RequestedAt/InstalledVersion/ConfirmedAt— no download tokens or credentials LoadAsyncthrowsInvalidDataExceptionfor corrupt JSON or an invalid record, so the component reportsFileIoErrorinstead of pretending there is no record- Only
ResetInstallationAsync()clears the record (including a corrupt one), and it never modifies the installed app
public interface IInstallationStore
{
Task<InstallationRecord?> LoadAsync(CancellationToken ct = default);
Task SaveAsync(InstallationRecord record, CancellationToken ct = default);
Task ClearAsync(CancellationToken ct = default);
}
10. Concurrency and Threading Model
10.1 SemaphoreSlim operation gate
private readonly SemaphoreSlim _operationGate = new(1, 1);
public async Task<UpdateCheckResult> ValidateAsync(string currentVersion, CancellationToken ct = default)
{
ThrowIfDisposed();
await _operationGate.WaitAsync(ct).ConfigureAwait(false);
try
{
ThrowIfDisposed();
return await ValidateCoreAsync(currentVersion, ct).ConfigureAwait(false);
}
finally
{
ReleaseOperation();
}
}
Design intent:
- All public operations (
PrepareUpdateAsync/ValidateAsync/DownloadAndVerifyAsync/LaunchInstallerAsync/CheckInstallationAsync/ResetInstallationAsync) share one gate, so only one runs at a time - Concurrent calls do not fail — they wait in line; cancellation while waiting throws
OperationCanceledException PrepareUpdateAsyncchains query, comparison, pre-check, download and verification inside a single critical section, so no other operation can rewrite state in between
10.2 Dispose lifetime
public void Dispose()
{
lock (_sync)
{
if (_disposed) return;
_disposed = true;
if (_operationGate.CurrentCount != 0) // no operation in flight → release immediately
DisposeResources();
}
}
| Behavior | Description |
|---|---|
Calling any method after Dispose | Throws ObjectDisposedException |
Dispose while an operation is running | Marks the instance disposed, rejects new and queued calls, and defers resource release until the active operation exits |
Does Dispose cancel the active operation | No; cancel the CancellationToken and await completion first |
| Release scope | Only downloader and package source that are IDisposable and owned by the bootstrap; host-owned dependencies such as the store remain the host's responsibility |
External HttpClient | Always host-owned; the component never disposes it (when no client is supplied the component creates and disposes its own) |
10.3 Thread-safe state snapshot
private readonly object _sync = new();
private UpdateStateSnapshot _snapshot = new(UpdateState.None, UpdateFailureReason.None, null);
private void SetState(UpdateState state, UpdateFailureReason failureReason, string? message)
{
lock (_sync)
{
_snapshot = new UpdateStateSnapshot(state, failureReason, message);
}
}
10.4 Message localization
Every built-in message passes through UpdateMessages.Get(language, message), where the language comes from
AndroidUpdateOptions.Language (English / Chinese). Branch on FailureReason, never on message text.
11. Event Dispatching and the UI Thread
11.1 IUpdateEventDispatcher
public interface IUpdateEventDispatcher
{
void Dispatch(Action callback);
}
The default ImmediateEventDispatcher invokes the delegate inline. Replace it with an Avalonia UI-thread dispatcher:
public sealed class AvaloniaEventDispatcher : IUpdateEventDispatcher
{
public void Dispatch(Action callback)
{
if (Avalonia.Threading.Dispatcher.UIThread.CheckAccess())
callback();
else
Avalonia.Threading.Dispatcher.UIThread.Post(callback);
}
}
All events (including AddListenerInstallationConfirmed) are raised through this dispatcher.
11.2 Event surface
| Event | EventArgs | Raised when |
|---|---|---|
AddListenerValidate | ValidateEventArgs (PackageInfo, CurrentVersion) | An update is available (not raised when the pre-check skips it) |
AddListenerDownloadProgressChanged | DownloadProgressChangedEventArgs | Download progress (speed, bytes, remaining, percentage, status) |
AddListenerUpdateCompleted | UpdateCompletedEventArgs (Result) | Download+verification finished (ReadyToInstall) or the installer was launched (Installing) |
AddListenerInstallationConfirmed | InstallationConfirmedEventArgs (Result) | CheckInstallationAsync first durably confirms the install |
AddListenerUpdateFailed | UpdateFailedEventArgs (Result) | Any step failed (including cancellation) |
11.3 Update pre-check
IAndroidBootstrap AddListenerUpdatePrecheck(Func<UpdateInfoEventArgs, bool> func);
UpdateInfoEventArgs carries PackageInfo, CurrentVersion, Result and IsForced. Returning true skips the update,
false continues; forced updates bypass the callback. The semantics match GeneralUpdate.Core's ClientStrategy.CanSkip.
12. Multi-Protocol Authentication
12.1 Schemes
public interface IHttpAuthProvider
{
Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default);
}
| Scheme | Implementation | Actual headers |
|---|---|---|
| HMAC-SHA256 | HmacAuthProvider | X-Update-Timestamp + X-Update-Signature (HMACSHA256 over body|timestamp) |
| Bearer Token | BearerTokenAuthProvider | Authorization: Bearer {token} |
| API Key | ApiKeyAuthProvider | X-Api-Key: {key} (header name is configurable) |
| HTTP Basic | BasicAuthProvider | Authorization: Basic {base64} |
| None | NoOpAuthProvider | Leaves the request untouched |
HttpAuthProviderFactory.Create(scheme, token, secretKey, basicUsername, basicPassword) builds one from an AuthScheme
or a string identifier.
12.2 Global vs per-package
// Global auth: passed through CreateDefault's httpOptions
var bootstrap = GeneralUpdateBootstrap.CreateDefault(
options,
httpOptions: new HttpDownloadOptions
{
AuthProvider = new BearerTokenAuthProvider("global-token")
});
// Per-package auth: comes from the server response or business data; takes precedence
var packageInfo = new UpdatePackageInfo
{
Version = "2.3.0",
DownloadUrl = "https://cdn.example.com/app.apk",
Sha256 = "……64 hex characters……",
AuthScheme = AuthScheme.Bearer,
AuthToken = "per-package-token"
};
12.3 Scope and security constraints
| Constraint | Description |
|---|---|
| Same-origin restriction | Global auth is only sent to HTTPS download URLs on the same origin as the verification endpoint; cross-origin downloads do not receive it |
| Plaintext HTTP | APK downloads over HTTP are rejected by default (InvalidMetadata); even with AllowInsecureHttpDownloads, global auth is never sent over HTTP |
| Per-package credentials | The authScheme / authToken from the server apply only to that package's download request |
| External client | With an external HttpClient, TLS/proxy must be configured on its handler; combining httpOptions TLS/proxy with a supplied client throws ArgumentException |
13. Key Code Path Index
| Component | File | Key members |
|---|---|---|
| Static factory | GeneralUpdateBootstrap.cs | CreateDefault() |
| Orchestrator | Services/AndroidBootstrap.cs | PrepareUpdateAsync() / ValidateAsync() / DownloadAndVerifyAsync() / LaunchInstallerAsync() / CheckInstallationAsync() / ResetInstallationAsync() |
| Orchestrator interface | Abstractions/IAndroidBootstrap.cs | — |
| Server package source | Services/HttpUpdatePackageClient.cs | GetLatestAsync() / GetPackageInfoAsync() / protocol mapping |
| Package source interface | Abstractions/IUpdatePackageSource.cs | GetLatestAsync() |
| Resumable downloader | Services/HttpResumableApkDownloader.cs | DownloadAsync() / HEAD probe / Range request / sliding-window speed |
| SHA256 validation | Services/Sha256HashValidator.cs | ValidateSha256Async() |
| APK installer | Services/AndroidApkInstaller.cs | LaunchInstallAsync() / FileProvider URI / install permission check |
| Version comparer | Services/SystemVersionComparer.cs | TryCompare() |
| File storage | Services/PhysicalFileStorage.cs | GetFileLength() / MoveFile() / DeleteFile() |
| Installation store | Services/JsonFileInstallationStore.cs | LoadAsync() / SaveAsync() (atomic) / ClearAsync() |
| Installation store interface | Abstractions/IInstallationStore.cs | — |
| Event dispatcher | Services/ImmediateEventDispatcher.cs | Dispatch() |
| HTTP client factory | Services/UpdateHttpClientFactory.cs | Create() / ownership and conflict detection |
| Download options | Models/HttpDownloadOptions.cs | SSL / proxy / timeout / retry / auth |
| Update options | Models/AndroidUpdateOptions.cs | UpdateServer / DownloadDirectory / InstallationStateFilePath / Language |
| Server options | Models/UpdateServerOptions.cs | Verification protocol and JSON endpoint configuration |
| Auth interface and providers | Abstractions/IHttpAuthProvider.cs, Services/AuthProviders.cs | ApplyAuthAsync() / HttpAuthProviderFactory |
| SSL policies | Abstractions/ISslValidationPolicy.cs, Services/SslValidationPolicies.cs | AllowAllSslValidationPolicy / StrictSslValidationPolicy |