Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README-EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ if (check.UpdateFound)
}
```

### Automatic server verification

Configure `AndroidUpdateOptions.UpdateServer` and call `ValidateAsync(currentVersion)`.
Server requests happen internally; `AddListenerUpdatePrecheck` receives the latest package and returns `true` to skip
or `false` to continue, matching GeneralUpdate. Forced updates bypass pre-check. Manual metadata remains supported.
See [automatic verification and GeneralSpacestation compatibility](src/GeneralUpdate.Avalonia.Android/README.md#automatic-server-verification-and-pre-check)
for examples, response formats and deployment requirements.

## Directory Structure

```text
Expand Down
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,87 @@ if (check.UpdateFound)
}
```

### 自动查询服务器并通过 precheck 决定是否更新

配置 `AndroidUpdateOptions.UpdateServer` 后,`ValidateAsync(currentVersion)` 会在组件内部请求服务器、
选择最新完整 APK 并比较版本。有更新时,通过 `AddListenerUpdatePrecheck` 提供最新包信息,由回调决定是否继续。
不需要创建或显式调用 HTTP 查询组件。

```csharp
var options = new AndroidUpdateOptions
{
FileProviderAuthority = "com.example.app.generalupdate.fileprovider",
UpdateServer = new UpdateServerOptions
{
RequestUrl = verificationUrl,
AppKey = appKey,
AppType = 1,
Platform = serverPlatformId,
ProductId = productId
}
};
using var bootstrap = GeneralUpdateBootstrap.CreateDefault(options);
bootstrap.AddListenerUpdatePrecheck(args =>
{
// args.PackageInfo:最新版本、下载地址、SHA256、Description 更新说明等。
// 与 GeneralUpdate 一致:true 跳过,false 继续;强制更新不调用此回调。
return false;
});

var check = await bootstrap.ValidateAsync("2.2.1", CancellationToken.None);
if (check.Success && check.UpdateFound && check.PackageInfo is { } packageInfo)
{
var prepared = await bootstrap.DownloadAndVerifyAsync(packageInfo, CancellationToken.None);
if (prepared.Success && prepared.FilePath is not null)
{
var install = await bootstrap.LaunchInstallerAsync(packageInfo, prepared.FilePath, CancellationToken.None);
// install.Success 表示已拉起系统安装器,不代表用户已完成安装。
}
}
```

回调返回 `true` 时,结果的 `UpdateFound` 为 `false`,不会触发 `AddListenerValidate`。
没有更新时也不调用 precheck。回调同步执行,不会自动切换到 UI 线程。
旧的 `ValidateAsync(packageInfo, currentVersion)` 重载保留,不请求服务器。

如果服务器提供静态 JSON,设置 `UpdateServer.UseJsonEndpoint = true` 并将 `RequestUrl` 指向 JSON 地址。
组件自动 GET `UpdatePackageInfo` JSON(字段名不区分大小写),例如:

```json
{
"version": "2.3.0",
"downloadUrl": "https://example.com/app-release.apk",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"description": "更新说明",
"isForced": false
}
```

`sha256` 必须替换为实际 APK 的 SHA-256,不能使用 MD5。`fileSize` 可省略或为 0(未知),已知时单位为字节。
HTTP 204 或 GET JSON `null` 表示无包;请求/协议错误通过 `UpdateCheckResult.Success = false`、
`FailureReason` 和 `AddListenerUpdateFailed` 通知,不会触发 precheck。请求期间取消会返回 `Canceled`;
等待操作锁时取消会抛出 `OperationCanceledException`,与已有重载一致。
仅应查询可信服务器,生产环境使用 HTTPS。

#### GeneralSpacestation / GeneralUpdate 验证协议

对于采用 [GeneralUpdate 示例服务器](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/Server)
`POST /Upgrade/Verification`(或 `/Update/Verification`)协议的部署,使用上述默认配置即可。

请求发送 `version/appKey/appType/platform/productId`,响应为 `{"code":200,"body":[...]}`。
客户端映射 `version/url/hash/size/name/updateLog/releaseDate/isForcibly/authScheme/authToken`,
按版本选取最新的非冻结完整 APK(`packageType` 为 2、0 或省略;`format` 为 `apk`/`.apk`,省略时 URL 路径须以 `.apk` 结尾)。
ZIP、差分包、驱动包不会交给 Android 安装器。`body: []` 或没有符合条件的包返回 `null`;
非 200 业务码、缺失或 `null` 的 `body` 会报告失败。默认使用 `System.Version` 比较版本,
也可通过 `CreateDefault` 的 `versionComparer` 同时替换包选择和本地版本比较逻辑。

GeneralSpacestation 商业服务的接口并未公开,**请核对实际部署的地址、响应格式和 Android 平台编号,不要假定固定编号**;
协议不同时可由服务器提供上面的标准 JSON 端点。查询与下载均使用 `CreateDefault` 的 `httpOptions`,
其中 `AuthProvider` 可使用现有
`BearerTokenAuthProvider`、`ApiKeyAuthProvider`、`BasicAuthProvider` 或 `HmacAuthProvider`。
`AppKey` 仅是请求字段,不会自动启用 HMAC。查询使用 `RequestTimeout`、代理和 TLS 配置;
不提供 `httpOptions` 时可复用传入的 `httpClient`,其生命周期仍由宿主管理。

## 目录结构

```text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/// <para>
/// Mirrors <c>GeneralUpdate.Core</c>'s <c>AddListenerUpdatePrecheck</c>: the callback receives
/// the update information (<see cref="UpdateInfoEventArgs"/>) and returns <c>true</c> to skip the
/// update or <c>false</c> to continue. When skipped, <see cref="ValidateAsync"/> returns

Check warning on line 21 in src/GeneralUpdate.Avalonia.Android/Abstractions/IAndroidBootstrap.cs

View workflow job for this annotation

GitHub Actions / Build and Pack Android Library

Ambiguous reference in cref attribute: 'ValidateAsync'. Assuming 'GeneralUpdate.Avalonia.Android.Abstractions.IAndroidBootstrap.ValidateAsync(string, System.Threading.CancellationToken)', but could have also matched other overloads including 'GeneralUpdate.Avalonia.Android.Abstractions.IAndroidBootstrap.ValidateAsync(GeneralUpdate.Avalonia.Android.Models.UpdatePackageInfo, string, System.Threading.CancellationToken)'.

Check warning on line 21 in src/GeneralUpdate.Avalonia.Android/Abstractions/IAndroidBootstrap.cs

View workflow job for this annotation

GitHub Actions / Build and Pack Android Library

Ambiguous reference in cref attribute: 'ValidateAsync'. Assuming 'GeneralUpdate.Avalonia.Android.Abstractions.IAndroidBootstrap.ValidateAsync(string, System.Threading.CancellationToken)', but could have also matched other overloads including 'GeneralUpdate.Avalonia.Android.Abstractions.IAndroidBootstrap.ValidateAsync(GeneralUpdate.Avalonia.Android.Models.UpdatePackageInfo, string, System.Threading.CancellationToken)'.

Check warning on line 21 in src/GeneralUpdate.Avalonia.Android/Abstractions/IAndroidBootstrap.cs

View workflow job for this annotation

GitHub Actions / Build and Pack Android Library

Ambiguous reference in cref attribute: 'ValidateAsync'. Assuming 'GeneralUpdate.Avalonia.Android.Abstractions.IAndroidBootstrap.ValidateAsync(string, System.Threading.CancellationToken)', but could have also matched other overloads including 'GeneralUpdate.Avalonia.Android.Abstractions.IAndroidBootstrap.ValidateAsync(GeneralUpdate.Avalonia.Android.Models.UpdatePackageInfo, string, System.Threading.CancellationToken)'.

Check warning on line 21 in src/GeneralUpdate.Avalonia.Android/Abstractions/IAndroidBootstrap.cs

View workflow job for this annotation

GitHub Actions / Build and Pack Android Library

Ambiguous reference in cref attribute: 'ValidateAsync'. Assuming 'GeneralUpdate.Avalonia.Android.Abstractions.IAndroidBootstrap.ValidateAsync(string, System.Threading.CancellationToken)', but could have also matched other overloads including 'GeneralUpdate.Avalonia.Android.Abstractions.IAndroidBootstrap.ValidateAsync(GeneralUpdate.Avalonia.Android.Models.UpdatePackageInfo, string, System.Threading.CancellationToken)'.
/// <see cref="UpdateCheckResult.UpdateFound"/> set to <c>false</c> with state <see cref="UpdateState.Completed"/>,
/// so callers naturally stop before downloading.
/// </para>
Expand All @@ -32,6 +32,15 @@
/// <exception cref="ArgumentNullException">Thrown when <paramref name="func"/> is null.</exception>
IAndroidBootstrap AddListenerUpdatePrecheck(Func<UpdateInfoEventArgs, bool> func);

/// <summary>
/// Queries the configured update server, compares versions and invokes the pre-check callback.
/// Returning true from pre-check skips the update; false continues. Forced updates bypass pre-check.
/// The returned PackageInfo can be passed to download and installation.
/// </summary>
Task<UpdateCheckResult> ValidateAsync(
string currentVersion,
CancellationToken cancellationToken = default);

Task<UpdateCheckResult> ValidateAsync(
UpdatePackageInfo packageInfo,
string currentVersion,
Expand Down
5 changes: 4 additions & 1 deletion src/GeneralUpdate.Avalonia.Android/GeneralUpdateBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ public static IAndroidBootstrap CreateDefault(
installer,
usedStorage,
eventDispatcher,
usedLogger);
usedLogger,
options.UpdateServer,
httpClient,
httpOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace GeneralUpdate.Avalonia.Android.Models;

public sealed record AndroidUpdateOptions
{
public UpdateServerOptions? UpdateServer { get; init; }
public string DownloadDirectoryPath { get; init; } = string.Empty;
public string TemporaryFileExtension { get; init; } = ".part";
public string SidecarExtension { get; init; } = ".json";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace GeneralUpdate.Avalonia.Android.Models;

/// <summary>
/// Configures HTTP transport behavior for update downloads:
/// Configures HTTP transport behavior for update verification and downloads:
/// SSL/TLS certificate validation, timeouts, proxy, retry, and authentication.
/// <para>
/// When provided to <see cref="GeneralUpdateBootstrap.CreateDefault"/>,
Expand All @@ -23,7 +23,7 @@ public sealed record HttpDownloadOptions
public ISslValidationPolicy? SslValidationPolicy { get; init; }

/// <summary>
/// Timeout for individual HTTP requests (HEAD probes, etc.).
/// Timeout for server verification requests and download HEAD probes.
/// Default is 30 seconds.
/// </summary>
public TimeSpan RequestTimeout { get; init; } = TimeSpan.FromSeconds(30);
Expand Down Expand Up @@ -61,8 +61,8 @@ public sealed record HttpDownloadOptions
public TimeSpan RetryBaseDelay { get; init; } = TimeSpan.FromSeconds(1);

/// <summary>
/// Global authentication provider applied to all download requests.
/// Per-package authentication on <see cref="UpdatePackageInfo"/> takes precedence.
/// Global authentication provider applied to verification and download requests.
/// Per-package authentication on <see cref="UpdatePackageInfo"/> takes precedence for downloads.
/// </summary>
public IHttpAuthProvider? AuthProvider { get; init; }

Expand Down
14 changes: 14 additions & 0 deletions src/GeneralUpdate.Avalonia.Android/Models/UpdatePackageRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace GeneralUpdate.Avalonia.Android.Models;

/// <summary>
/// Request body for the GeneralUpdate /Upgrade/Verification reference protocol.
/// Platform must match the identifier configured by the server.
/// </summary>
internal sealed record UpdatePackageRequest
{
public required string Version { get; init; }
public required string AppKey { get; init; }
public int AppType { get; init; } = 1;
public required int Platform { get; init; }
public required string ProductId { get; init; }
}
18 changes: 18 additions & 0 deletions src/GeneralUpdate.Avalonia.Android/Models/UpdateServerOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace GeneralUpdate.Avalonia.Android.Models;

/// <summary>
/// Configures the server queried internally by version validation.
/// </summary>
public sealed record UpdateServerOptions
{
public required string RequestUrl { get; init; }
public string AppKey { get; init; } = string.Empty;
public int AppType { get; init; } = 1;
public int Platform { get; init; }
public string ProductId { get; init; } = string.Empty;

/// <summary>
/// GET a JSON UpdatePackageInfo instead of POSTing the GeneralUpdate verification protocol.
/// </summary>
public bool UseJsonEndpoint { get; init; }
}
7 changes: 7 additions & 0 deletions src/GeneralUpdate.Avalonia.Android/README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,17 @@ Default wiring:
| `IApkInstaller` | `AndroidApkInstaller` |
| `IVersionComparer` | `SystemVersionComparer` |

### Automatic server verification

Configure `AndroidUpdateOptions.UpdateServer` and call `ValidateAsync(currentVersion)` to query the server internally.
`AddListenerUpdatePrecheck` receives the latest package; return `true` to skip or `false` to continue (forced updates bypass it).
See [examples and GeneralSpacestation compatibility](README.md#automatic-server-verification-and-pre-check).

### IAndroidBootstrap Methods

| Method | Returns |
|---|---|
| `ValidateAsync(currentVersion, ct)` | `UpdateCheckResult`, including the discovered `PackageInfo` |
| `ValidateAsync(packageInfo, currentVersion, ct)` | `UpdateCheckResult` |
| `DownloadAndVerifyAsync(packageInfo, ct)` | `UpdateOperationResult` |
| `LaunchInstallerAsync(packageInfo, apkFilePath, ct)` | `InstallResult` |
Expand Down
79 changes: 79 additions & 0 deletions src/GeneralUpdate.Avalonia.Android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,84 @@ if (check.UpdateFound)
}
```

## Automatic server verification and pre-check

Configure `AndroidUpdateOptions.UpdateServer`, then call `ValidateAsync(currentVersion)`.
The component requests metadata internally, selects the latest full APK and compares versions before invoking
`AddListenerUpdatePrecheck` with the latest package information. No explicit HTTP client calls are required.

```csharp
var options = new AndroidUpdateOptions
{
FileProviderAuthority = "com.example.app.generalupdate.fileprovider",
UpdateServer = new UpdateServerOptions
{
RequestUrl = verificationUrl,
AppKey = appKey,
AppType = 1,
Platform = serverPlatformId,
ProductId = productId
}
};
using var bootstrap = GeneralUpdateBootstrap.CreateDefault(options);
bootstrap.AddListenerUpdatePrecheck(args =>
{
// Inspect args.PackageInfo: latest version, Description/release notes, URL, SHA256, etc.
return false; // true skips, false continues; forced updates bypass this callback.
});
var check = await bootstrap.ValidateAsync(currentVersion, ct);
if (check.Success && check.UpdateFound && check.PackageInfo is { } packageInfo)
{
var prepared = await bootstrap.DownloadAndVerifyAsync(packageInfo, ct);
if (prepared.Success && prepared.FilePath is not null)
{
var install = await bootstrap.LaunchInstallerAsync(packageInfo, prepared.FilePath, ct);
// Success means the system installer was launched, not that installation completed.
}
}
```

Returning `true` produces `UpdateFound == false` and suppresses `AddListenerValidate`. No-update checks do not invoke pre-check.
The callback is synchronous and is not automatically dispatched to the UI thread.
The existing `ValidateAsync(packageInfo, currentVersion)` overload remains available and never queries the server.

For a static JSON endpoint, set `UpdateServer.UseJsonEndpoint = true` and point `RequestUrl` at a JSON `UpdatePackageInfo`
(case-insensitive property names). The component performs the GET internally:

```json
{
"version": "2.3.0",
"downloadUrl": "https://example.com/app-release.apk",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"description": "Release notes",
"isForced": false
}
```

Replace `sha256` with the actual APK's SHA-256, never an MD5 digest. Optional `fileSize` is in bytes; zero means unknown.
HTTP 204 or GET JSON `null` produces a successful no-update result. HTTP/protocol/metadata failures produce
`Success == false`, a `FailureReason` and `AddListenerUpdateFailed`, not pre-check.
Cancellation during the request returns `Canceled`; cancellation while waiting for the operation lock throws
`OperationCanceledException`, matching the existing overload. Use trusted endpoints and HTTPS in production.

### GeneralSpacestation / GeneralUpdate reference protocol

For deployments implementing the [GeneralUpdate sample server](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/src/Server)
`POST /Upgrade/Verification` (or `/Update/Verification`) contract, use the default configuration shown above.

The request uses `version/appKey/appType/platform/productId`. The response must be `{"code":200,"body":[...]}`.
The client maps `version/url/hash/size/name/updateLog/releaseDate/isForcibly/authScheme/authToken` to package metadata and selects
the newest non-frozen full APK. `packageType` must be 2, 0 or absent; `format` must be `apk`/`.apk`, or, if omitted, the URL path
must end in `.apk`. ZIP, differential and driver packages are ignored. An empty eligible list means no update;
an unsuccessful application code or missing/null `body` reports a failure. Version ordering defaults to `System.Version`;
pass `versionComparer` to `CreateDefault` to customize both remote selection and local comparison.

The commercial GeneralSpacestation API is not publicly specified: **verify your deployment's route, schema and Android platform identifier**.
Do not assume a fixed Android platform number. For other protocols, expose the standard JSON endpoint shown above.
`CreateDefault`'s `httpOptions` configures both requests and downloads: `AuthProvider` (Bearer, API key, Basic or HMAC),
TLS and proxy settings. Verification uses `RequestTimeout`. `AppKey` is only a request field; it does not automatically enable HMAC.
Without `httpOptions`, an injected `httpClient` is reused and remains owned by the caller.

## API

### Static Factory
Expand Down Expand Up @@ -88,6 +166,7 @@ public static IAndroidBootstrap CreateDefault(

| Method | Description |
|---|---|
| `ValidateAsync(currentVersion, ct)` | Query the configured server internally, compare versions and apply pre-check; return metadata in `UpdateCheckResult.PackageInfo` |
| `ValidateAsync(packageInfo, currentVersion, ct)` | Compare versions, fire `AddListenerValidate` or `AddListenerUpdateFailed`, return `UpdateCheckResult` |
| `DownloadAndVerifyAsync(packageInfo, ct)` | Resume-download APK, SHA-256 verify, fire progress/completed/failed events, return `UpdateOperationResult` |
| `LaunchInstallerAsync(packageInfo, apkFilePath, ct)` | Launch Android `ACTION_VIEW` intent via FileProvider, return `InstallResult` |
Expand Down
7 changes: 7 additions & 0 deletions src/GeneralUpdate.Avalonia.Android/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,17 @@ if (check.UpdateFound)
| `IApkInstaller` | `AndroidApkInstaller` |
| `IVersionComparer` | `SystemVersionComparer` |

### 自动查询服务器

配置 `AndroidUpdateOptions.UpdateServer` 后调用 `ValidateAsync(currentVersion)`,组件内部自动请求服务器。
`AddListenerUpdatePrecheck` 回调提供最新包信息,返回 `true` 跳过、`false` 继续(强制更新不调用此回调)。
参见[中文使用示例与 GeneralSpacestation 兼容性说明](../../README.md#自动查询服务器并通过-precheck-决定是否更新)。

### IAndroidBootstrap 方法

| 方法 | 返回类型 |
|---|---|
| `ValidateAsync(currentVersion, ct)` | `UpdateCheckResult`,包含查询到的 `PackageInfo` |
| `ValidateAsync(packageInfo, currentVersion, ct)` | `UpdateCheckResult` |
| `DownloadAndVerifyAsync(packageInfo, ct)` | `UpdateOperationResult` |
| `LaunchInstallerAsync(packageInfo, apkFilePath, ct)` | `InstallResult` |
Expand Down
Loading
Loading