Skip to main content

GeneralUpdate.Avalonia.Android — Execution Flow Deep Dive

Target Audience: Developers integrating auto-update into Avalonia Android apps

After reading you will understand:

  • How ValidateAsync queries the server and picks the newest APK (the caller only supplies the current version)
  • How to choose between PrepareUpdateAsync and the phased three-step API
  • HttpResumableApkDownloader internals: 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 and Dispose lifetime contracts
  • IUpdateEventDispatcher UI thread dispatch mechanism
  • Multi-protocol authentication architecture (HMAC / Bearer / API Key / Basic) and where credentials are sent

Table of Contents​

  1. Architecture overview
  2. Entry point: the GeneralUpdateBootstrap factory
  3. Two calling styles: one-shot and phased
  4. ValidateAsync — server query and version validation
  5. DownloadAndVerifyAsync — download and verification
  6. Resumable download: HttpResumableApkDownloader internals
  7. Hash and file size verification
  8. LaunchInstallerAsync — APK installation handoff
  9. The installation result loop
  10. Concurrency and threading model
  11. Event dispatching and the UI thread
  12. Multi-protocol authentication
  13. 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​

StyleMethodWhen to use
One-shotPrepareUpdateAsync(currentVersion, ct)The host only needs "a verified APK ready to install"; query, comparison, pre-check, download and verification run under one operation lock
PhasedValidateAsync → DownloadAndVerifyAsync → LaunchInstallerAsyncYou need to show a prompt after the check, a progress bar during download, and ask for confirmation before installing
StepMethodResponsibilityCaller control point
0ValidateAsync(currentVersion, ct)Query the server + compare versions + pre-checkDecide whether to continue
1DownloadAndVerifyAsync(packageInfo, ct)Download + size check + SHA256 checkShow progress UI
2LaunchInstallerAsync(packageInfo, path, ct)Persist the installation record + launch the system installerCall after user confirmation
—CheckInstallationAsync(currentVersion, ct)Offline reconciliation of the previous installCall at startup / on return
Key difference from earlier versions

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:

  1. The download directory and installation record path are written back into options (through a with copy) so the downloader and installer share the same resolved paths
  2. DownloadBufferSize <= 0 throws ArgumentOutOfRangeException immediately; when the Android context is unavailable and InstallationStateFilePath was not set explicitly, an InvalidOperationException is 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:

ModeTriggerBehavior
GeneralUpdate verification protocolUpdateServer.UseJsonEndpoint = false (default)POST body version / appKey / appType / platform / productId, response {"code":200,"body":[...]}
Static JSONUpdateServer.UseJsonEndpoint = trueGET 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​

ExceptionReported result
HttpRequestException / OperationCanceledException / IOException / TimeoutExceptionFailureReason = NetworkError
JsonException / InvalidDataException / ArgumentExceptionFailureReason = InvalidMetadata
No UpdateServer and no injected package sourceInvalidDataException("Configure UpdateServer or supply an IUpdatePackageSource.") → InvalidMetadata
Caller cancellation during the requestState = Canceled, FailureReason = Canceled
Caller cancellation while waiting for the operation lockOperationCanceledException is thrown (no result object)
Query failures never trigger the pre-check

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​

SituationRetried
Transient network failures on HEAD / GET / response stream (timeout, 429, 5xx, no status code)Yes, with resume
Metadata query failuresNo
Permanent HTTP errors (for example 401)No
Caller cancellationNo

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​

Persist the record before launching the installer

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​

ResultMeaningSuggested handling
Success && State == NoneNo installation recordNo prompt; continue with the normal update check
HasPendingInstallationThe installed version is still below the targetRe-check and retry; never reset the record automatically
IsInstalledThe device reached or exceeded the targetReport success; the confirmation is persisted and will not be raised again
!SuccessInvalid device version, or record read/write failureReported 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
  • LoadAsync throws InvalidDataException for corrupt JSON or an invalid record, so the component reports FileIoError instead 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
  • PrepareUpdateAsync chains 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();
}
}
BehaviorDescription
Calling any method after DisposeThrows ObjectDisposedException
Dispose while an operation is runningMarks the instance disposed, rejects new and queued calls, and defers resource release until the active operation exits
Does Dispose cancel the active operationNo; cancel the CancellationToken and await completion first
Release scopeOnly 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 HttpClientAlways 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​

EventEventArgsRaised when
AddListenerValidateValidateEventArgs (PackageInfo, CurrentVersion)An update is available (not raised when the pre-check skips it)
AddListenerDownloadProgressChangedDownloadProgressChangedEventArgsDownload progress (speed, bytes, remaining, percentage, status)
AddListenerUpdateCompletedUpdateCompletedEventArgs (Result)Download+verification finished (ReadyToInstall) or the installer was launched (Installing)
AddListenerInstallationConfirmedInstallationConfirmedEventArgs (Result)CheckInstallationAsync first durably confirms the install
AddListenerUpdateFailedUpdateFailedEventArgs (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);
}
SchemeImplementationActual headers
HMAC-SHA256HmacAuthProviderX-Update-Timestamp + X-Update-Signature (HMACSHA256 over body|timestamp)
Bearer TokenBearerTokenAuthProviderAuthorization: Bearer {token}
API KeyApiKeyAuthProviderX-Api-Key: {key} (header name is configurable)
HTTP BasicBasicAuthProviderAuthorization: Basic {base64}
NoneNoOpAuthProviderLeaves 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​

ConstraintDescription
Same-origin restrictionGlobal auth is only sent to HTTPS download URLs on the same origin as the verification endpoint; cross-origin downloads do not receive it
Plaintext HTTPAPK downloads over HTTP are rejected by default (InvalidMetadata); even with AllowInsecureHttpDownloads, global auth is never sent over HTTP
Per-package credentialsThe authScheme / authToken from the server apply only to that package's download request
External clientWith 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​

ComponentFileKey members
Static factoryGeneralUpdateBootstrap.csCreateDefault()
OrchestratorServices/AndroidBootstrap.csPrepareUpdateAsync() / ValidateAsync() / DownloadAndVerifyAsync() / LaunchInstallerAsync() / CheckInstallationAsync() / ResetInstallationAsync()
Orchestrator interfaceAbstractions/IAndroidBootstrap.cs—
Server package sourceServices/HttpUpdatePackageClient.csGetLatestAsync() / GetPackageInfoAsync() / protocol mapping
Package source interfaceAbstractions/IUpdatePackageSource.csGetLatestAsync()
Resumable downloaderServices/HttpResumableApkDownloader.csDownloadAsync() / HEAD probe / Range request / sliding-window speed
SHA256 validationServices/Sha256HashValidator.csValidateSha256Async()
APK installerServices/AndroidApkInstaller.csLaunchInstallAsync() / FileProvider URI / install permission check
Version comparerServices/SystemVersionComparer.csTryCompare()
File storageServices/PhysicalFileStorage.csGetFileLength() / MoveFile() / DeleteFile()
Installation storeServices/JsonFileInstallationStore.csLoadAsync() / SaveAsync() (atomic) / ClearAsync()
Installation store interfaceAbstractions/IInstallationStore.cs—
Event dispatcherServices/ImmediateEventDispatcher.csDispatch()
HTTP client factoryServices/UpdateHttpClientFactory.csCreate() / ownership and conflict detection
Download optionsModels/HttpDownloadOptions.csSSL / proxy / timeout / retry / auth
Update optionsModels/AndroidUpdateOptions.csUpdateServer / DownloadDirectory / InstallationStateFilePath / Language
Server optionsModels/UpdateServerOptions.csVerification protocol and JSON endpoint configuration
Auth interface and providersAbstractions/IHttpAuthProvider.cs, Services/AuthProviders.csApplyAuthAsync() / HttpAuthProviderFactory
SSL policiesAbstractions/ISslValidationPolicy.cs, Services/SslValidationPolicies.csAllowAllSslValidationPolicy / StrictSslValidationPolicy