diff --git a/website/docs/doc/Component Introduction.md b/website/docs/doc/Component Introduction.md index e8ca3db..df42577 100644 --- a/website/docs/doc/Component Introduction.md +++ b/website/docs/doc/Component Introduction.md @@ -91,7 +91,8 @@ GeneralUpdate is an open-source cross-platform application automatic update comp | Name | Description | Address | | --------------------- | -------------------------- | ------------------------------------------------------------ | | GeneralUpdate | Automatic Updates | [GitHub](https://github.com/GeneralLibrary/GeneralUpdate)
[Gitee](https://gitee.com/GeneralLibrary/GeneralUpdate)
[GitCode](https://gitcode.com/GeneralLibrary/GeneralUpdate) | -| GeneralUpdate.Maui | Maui Updates (Android) | [GitHub](https://github.com/GeneralLibrary/GeneralUpdate.Maui)
[Gitee](https://gitee.com/GeneralLibrary/GeneralUpdate.Maui)
[GitCode](https://gitcode.com/GeneralLibrary/GeneralUpdate-Maui) | +| GeneralUpdate.Maui | MAUI Android Auto-Updates | [GitHub](https://github.com/GeneralLibrary/GeneralUpdate.Maui)
[Gitee](https://gitee.com/GeneralLibrary/GeneralUpdate.Maui)
[GitCode](https://gitcode.com/GeneralLibrary/GeneralUpdate-Maui) | +| GeneralUpdate.Avalonia | Avalonia Android Auto-Updates | [GitHub](https://github.com/GeneralLibrary/GeneralUpdate.Avalonia) | | GeneralUpdate.Tools | Update Patch Creation Tool | [GitHub](https://github.com/GeneralLibrary/GeneralUpdate.Tools)
[Gitee](https://gitee.com/GeneralLibrary/GeneralUpdate.Tools)
[GitCode](https://gitcode.com/GeneralLibrary/GeneralUpdate-Tools) | | GeneralUpdate-Samples | Usage Examples | [GitHub](https://github.com/GeneralLibrary/GeneralUpdate-Samples)
[Gitee](https://gitee.com/GeneralLibrary/GeneralUpdate-Samples)
[GitCode](https://gitcode.com/GeneralLibrary/GeneralUpdate-Samples) | diff --git a/website/docs/doc/GeneralUpdate.Avalonia.Android.md b/website/docs/doc/GeneralUpdate.Avalonia.Android.md new file mode 100644 index 0000000..5ece37c --- /dev/null +++ b/website/docs/doc/GeneralUpdate.Avalonia.Android.md @@ -0,0 +1,653 @@ +--- +sidebar_position: 9 +--- + +# GeneralUpdate.Avalonia.Android + +**命名空间:** `GeneralUpdate.Avalonia.Android` | **主要入口:** `GeneralUpdateBootstrap.CreateDefault()` | **NuGet 包:** `GeneralUpdate.Avalonia.Android` + +## 1. 组件简介 + +### 1.1 组件概述 + +**GeneralUpdate.Avalonia.Android** 是面向 Avalonia 应用的 Android 平台自动更新组件,专为 Android APK 的更新场景设计。它提供版本对比、可断点续传的 APK 下载、SHA256 完整性校验、以及 Android Package Installer 自动启动的全流程编排能力。 + +Android 平台的更新与桌面应用不同:APK 安装必须通过 Android 系统安装器完成,下载过程可能被用户中断或切换网络,安装前需要检查 "安装未知应用" 权限。该组件封装了这些平台差异,让 Avalonia 开发者只需提供更新包信息即可完成更新。 + +**核心能力:** + +| 能力 | 说明 | +| --- | --- | +| 版本对比 | 内置 `SystemVersionComparer`,通过 `IVersionComparer` 接口支持自定义版本策略 | +| 断点续传下载 | `HttpResumableApkDownloader` 支持 HTTP Range 请求头,中断后可续传 | +| SHA256 完整性校验 | 下载完成后自动计算文件 SHA256,与服务端声明的哈希值比对,防止文件损坏或篡改 | +| APK 安装编排 | 通过 `AndroidApkInstaller` 调用系统 Package Installer,自动处理 FileProvider URI | +| HTTP 传输配置 | `HttpDownloadOptions` 支持 SSL/TLS 证书策略、代理、超时、重试和多种认证方案 | +| 多协议认证 | 支持 HMAC-SHA256、Bearer Token、API Key、HTTP Basic 四种认证方案,支持全局和单包粒度 | +| 事件通知 | 版本发现、下载进度、完成、失败等事件通知 | +| 状态快照 | `GetSnapshot()` 随时获取当前更新状态、失败原因和消息 | +| UI 线程调度 | `IUpdateEventDispatcher` 支持将事件调度到 UI 线程(如 Avalonia 的 `SynchronizationContext`) | +| 渐进式下载 | 临时文件 `.part` 和 Sidecar `.json` 机制,记录已完成字节数,支持进程级断点续传 | + +**解决的业务痛点:** +- Avalonia 应用发布 Android 版本后,需要可靠的内置更新能力,但 Android APK 安装必须经由系统安装器 +- 下载大 APK 包时可能被用户中断、网络切换,需要断点续传能力降低重复下载流量 +- 需要对下载的 APK 进行完整性校验,防止传输损坏或被篡改 +- 需要灵活的认证方案对接不同服务端的鉴权要求 + +:::info Android 更新 ≠ 桌面文件替换 +Android 平台不能像桌面端那样直接替换文件。APK 必须通过 Android Package Installer 安装,系统会处理签名验证、权限授予和应用替换。本组件封装了这些平台差异,但 **APK 签名、版本号递增等仍需要你在构建流水线中保证**。 +::: + +**业务使用场景:** +- Avalonia 跨平台应用的 Android APK 自动更新 +- 企业内部 Android 应用的静默或引导式更新 +- 需要通过 CDN / OSS 分发 APK 包的场景 + +### 1.2 环境与依赖 + +| 项目 | 说明 | +| --- | --- | +| **NuGet 包** | `GeneralUpdate.Avalonia.Android` | +| **目标框架** | `net10.0-android`(最低 Android API 26+) | +| **依赖包** | `Xamarin.AndroidX.Core`(FileProvider 支持) | +| **兼容性** | Android 8.0 (API 26) 及以上版本 | + +--- + +## 2. 组件功能列表 + +| 功能名称 | 功能描述 | 类型 | 是否必填 | 备注限制 | +| --- | --- | --- | --- | --- | +| 版本对比检查 | 比对当前版本与目标版本,判断是否有更新可用 | 基础 | 必选 | `ValidateAsync`,支持自定义版本比较器 | +| APK 断点续传下载 | 从服务端下载 APK 包,支持 HTTP Range 断点续传 | 基础 | 必选 | `DownloadAndVerifyAsync` | +| SHA256 校验 | 下载完成后自动计算 SHA256 与服务端哈希比对 | 基础 | 自动 | 通过 `IHashValidator` 实现 | +| APK 安装触发 | 调用 Android Package Installer 安装 APK | 基础 | 必选 | `LaunchInstallerAsync` | +| HTTP 传输配置 | SSL 证书校验、代理、超时、重试、认证 | 拓展 | 可选 | `HttpDownloadOptions` | +| 多协议认证 | HMAC-SHA256 / Bearer / API Key / Basic | 拓展 | 可选 | 全局或单包粒度配置 | +| 进度通知 | 下载进度(速度、字节数、百分比) | 基础 | 可选 | `AddListenerDownloadProgressChanged` | +| 完成通知 | 版本发现、下载完成、安装完成事件 | 基础 | 可选 | `AddListenerUpdateCompleted` | +| 失败通知 | 失败原因、异常信息、失败包信息 | 基础 | 可选 | `AddListenerUpdateFailed` | +| 验证通知 | 发现可用更新时触发 | 基础 | 可选 | `AddListenerValidate` | +| 状态快照 | 随时获取当前更新状态 | 拓展 | 可选 | `GetSnapshot()` | +| UI 线程调度 | 自定义事件调度策略 | 拓展 | 可选 | 实现 `IUpdateEventDispatcher` | +| 自定义下载器 | 自定义下载实现 | 拓展 | 可选 | 实现 `IUpdateDownloader` | +| 自定义哈希验证 | 自定义哈希算法 | 拓展 | 可选 | 实现 `IHashValidator` | +| 自定义版本比较 | 自定义版本对比逻辑 | 拓展 | 可选 | 实现 `IVersionComparer` | +| 自定义 APK 安装器 | 自定义安装实现 | 拓展 | 可选 | 实现 `IApkInstaller` | +| 自定义文件存储 | 自定义文件读写实现 | 拓展 | 可选 | 实现 `IFileStorage` | +| 自定义 SSL 策略 | 自定义 HTTPS 证书校验 | 拓展 | 可选 | 实现 `ISslValidationPolicy` | +| 自定义 HTTP 认证 | 自定义 HTTP 请求认证 | 拓展 | 可选 | 实现 `IHttpAuthProvider` | + +--- + +## 3. API 配置说明 + +### 3.1 配置字段(属性 Props) + +**AndroidUpdateOptions:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 说明 | +| --- | --- | --- | --- | --- | +| `DownloadDirectoryPath` | `string` | `""`(自动使用 `CacheDir/update` 或 `TempPath/update`) | 可选 | 下载文件存放目录,为空时自动选择 | +| `FileProviderAuthority` | `string` | `""` | **是** | Android FileProvider authority,需与 AndroidManifest 配置一致 | +| `TemporaryFileExtension` | `string` | `".part"` | 可选 | 临时下载文件扩展名 | +| `SidecarExtension` | `string` | `".json"` | 可选 | 断点续传元数据文件扩展名 | +| `DownloadBufferSize` | `int` | `65536`(64 KB) | 可选 | 下载缓冲区大小(字节) | +| `SpeedSmoothingWindowSeconds` | `int` | `4` | 可选 | 下载速度平滑窗口(秒) | + +**HttpDownloadOptions:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 说明 | +| --- | --- | --- | --- | --- | +| `SslValidationPolicy` | `ISslValidationPolicy?` | `null` | 可选 | 自定义 SSL 证书验证策略;`null` 使用系统默认 | +| `RequestTimeout` | `TimeSpan` | `30s` | 可选 | 单次 HTTP 请求超时 | +| `DownloadTimeout` | `TimeSpan` | `10min` | 可选 | 整体下载操作超时 | +| `Proxy` | `IWebProxy?` | `null` | 可选 | HTTP 代理 | +| `UseProxy` | `bool` | `false` | 可选 | 是否启用配置的代理 | +| `MaxRetryAttempts` | `int` | `3` | 可选 | 最大重试次数(3 表示初始 1 次 + 2 次重试) | +| `RetryBaseDelay` | `TimeSpan` | `1s` | 可选 | 指数退避基本延迟:`baseDelay * 2^attempt` | +| `AuthProvider` | `IHttpAuthProvider?` | `null` | 可选 | 全局 HTTP 认证提供器,单包认证优先级更高 | + +**UpdatePackageInfo:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 说明 | +| --- | --- | --- | --- | --- | +| `Version` | `string` | — | **是** | 目标版本号 | +| `DownloadUrl` | `string` | — | **是** | APK 下载地址 | +| `Sha256` | `string` | — | **是** | APK 的 SHA256 哈希值,用于完整性校验 | +| `FileSize` | `long` | `0` | 可选 | APK 文件大小(字节) | +| `FileName` | `string?` | `null` | 可选 | 下载后的文件名 | +| `IsForced` | `bool` | `false` | 可选 | 是否强制更新 | +| `VersionName` | `string?` | `null` | 可选 | 版本展示名称 | +| `Description` | `string?` | `null` | 可选 | 版本描述 | +| `PublishTime` | `DateTimeOffset?` | `null` | 可选 | 发布时间 | +| `AuthScheme` | `AuthScheme?` | `null` | 可选 | 单包认证方案,优先于全局配置 | +| `AuthToken` | `string?` | `null` | 可选 | Bearer 或 API Key 认证令牌 | +| `AuthSecretKey` | `string?` | `null` | 可选 | HMAC-SHA256 签名密钥 | +| `BasicUsername` | `string?` | `null` | 可选 | HTTP Basic 用户名 | +| `BasicPassword` | `string?` | `null` | 可选 | HTTP Basic 密码 | + +**UpdateState 枚举:** + +| 枚举值 | 数值 | 说明 | +| --- | --- | --- | +| `None` | 0 | 初始状态 | +| `Checking` | 1 | 正在检查更新 | +| `UpdateAvailable` | 2 | 发现可用更新 | +| `Downloading` | 3 | 正在下载 | +| `Verifying` | 4 | 正在校验 | +| `ReadyToInstall` | 5 | 准备安装 | +| `Installing` | 6 | 正在安装 | +| `Completed` | 7 | 更新完成 | +| `Failed` | 8 | 更新失败 | +| `Canceled` | 9 | 已取消 | + +**AuthScheme 枚举:** + +| 枚举值 | 说明 | +| --- | --- | +| `Hmac` | HMAC-SHA256 签名认证 | +| `Bearer` | Bearer Token 认证 | +| `ApiKey` | API Key 认证 | +| `Basic` | HTTP Basic 认证 | + +### 3.2 实例方法 + +**GeneralUpdateBootstrap(静态工厂):** + +| 方法名 | 入参明细 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | +| `CreateDefault(AndroidUpdateOptions, ...)` | `options` — 更新选项;可选参数:`contextProvider`, `activityProvider`, `httpClient`, `versionComparer`, `eventDispatcher`, `logger`, `httpOptions` | 创建默认的 Android 更新引导实例 | 所有可选参数为 null 时使用内置默认实现 | + +**IAndroidBootstrap:** + +| 方法名 | 入参明细 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | +| `ValidateAsync(UpdatePackageInfo, string, CancellationToken)` | `packageInfo` — 更新包信息;`currentVersion` — 当前版本;`ct` — 取消令牌 | 检查是否有可用的更新 | 返回 `UpdateCheckResult`,包含 `UpdateFound` 和 `TargetVersion` | +| `DownloadAndVerifyAsync(UpdatePackageInfo, CancellationToken)` | `packageInfo` — 更新包信息;`ct` — 取消令牌 | 下载 APK 并校验 SHA256 | 返回 `UpdateOperationResult`,成功时 `FilePath` 为 APK 路径 | +| `LaunchInstallerAsync(UpdatePackageInfo, string, CancellationToken)` | `packageInfo` — 更新包信息;`apkFilePath` — APK 文件路径;`ct` — 取消令牌 | 启动 Android Package Installer | 需要 FileProvider authority 配置正确 | +| `GetSnapshot()` | 无 | 获取当前更新状态快照 | 返回 `UpdateStateSnapshot(State, FailureReason, Message)` | +| `Dispose()` | 无 | 释放下载器等资源 | — | + +### 3.3 回调事件 + +| 事件名称 | 回调参数 | 触发时机 | 使用说明 | +| --- | --- | --- | --- | +| `AddListenerValidate` | `ValidateEventArgs` — `PackageInfo`, `CurrentVersion` | 版本对比完成后,发现可用更新时触发 | 可用于 UI 展示新版本信息 | +| `AddListenerDownloadProgressChanged` | `DownloadProgressChangedEventArgs` — `ProgressPercentage`, `DownloadSpeedBytesPerSecond`, `DownloadedBytes`, `RemainingBytes`, `TotalBytes`, `PackageInfo`, `StatusDescription` | 下载进度更新时 | 可用于进度条展示 | +| `AddListenerUpdateCompleted` | `UpdateCompletedEventArgs` — `Result`(`UpdateOperationResult`) | 下载完成或安装完成时 | 可用于 UI 切换 | +| `AddListenerUpdateFailed` | `UpdateFailedEventArgs` — `Result`(`UpdateOperationResult`) | 任何阶段失败时 | 包含失败原因和异常信息 | + +--- + +## 4. 扩展示例(高阶用法) + +### 4.1 组件可扩展能力总览 + +所有服务均可通过 `CreateDefault` 的可选参数替换: + +| 接口 | 默认实现 | 说明 | +| --- | --- | --- | +| `IVersionComparer` | `SystemVersionComparer` | 版本对比策略 | +| `IUpdateDownloader` | `HttpResumableApkDownloader` | APK 下载实现 | +| `IHashValidator` | `Sha256HashValidator` | SHA256 哈希校验 | +| `IApkInstaller` | `AndroidApkInstaller` | APK 安装触发 | +| `IFileStorage` | `PhysicalFileStorage` | 文件存储操作 | +| `IUpdateEventDispatcher` | `ImmediateEventDispatcher` | 事件调度器(直接调用) | +| `IUpdateLogger` | `NoOpUpdateLogger` | 日志记录器 | +| `IAndroidContextProvider` | `DefaultAndroidContextProvider` | Android Context 提供器 | +| `IAndroidActivityProvider` | `NullAndroidActivityProvider` | Android Activity 提供器 | +| `ISslValidationPolicy` | 通过 `HttpDownloadOptions` 配置 | SSL 证书验证策略 | +| `IHttpAuthProvider` | 通过 `HttpDownloadOptions` 或单包配置 | HTTP 认证提供器 | + +### 4.2 分场景示例 + +#### 场景 1:自定义版本比较策略 + +【场景说明】应用使用 `year.month.day.build` 格式版本号,默认的 `System.Version` 解析会失败,需要自定义版本比较器。 + +【示例代码】 + +```csharp +using GeneralUpdate.Avalonia.Android.Abstractions; + +public sealed class CustomDateVersionComparer : IVersionComparer +{ + public bool TryCompare(string currentVersion, string targetVersion, + out int compareResult, out string? errorMessage) + { + try + { + var current = ParseVersion(currentVersion); + var target = ParseVersion(targetVersion); + compareResult = target.CompareTo(current); + errorMessage = null; + return true; + } + catch (Exception ex) + { + compareResult = 0; + errorMessage = ex.Message; + return false; + } + } + + private static DateTime ParseVersion(string version) => + DateTime.ParseExact(version, "yyyy.MM.dd.fff", null); +} + +// 使用 +var bootstrap = GeneralUpdateBootstrap.CreateDefault( + options, + versionComparer: new CustomDateVersionComparer()); +``` + +【效果&注意事项】 +- 返回的 `compareResult` 遵循 `target.CompareTo(current)` 语义:正数表示有新版本 +- 实现失败时返回 `false` 和错误信息,引导跳过更新检查 + +#### 场景 2:自定义事件调度器(Avalonia UI 线程调度) + +【场景说明】在 Avalonia 应用中,事件回调默认在后台线程触发,更新 UI 需要调度到 UI 线程。 + +```csharp +using GeneralUpdate.Avalonia.Android.Abstractions; + +public sealed class AvaloniaEventDispatcher : IUpdateEventDispatcher +{ + public void Dispatch(Action callback) + { + if (Avalonia.Threading.Dispatcher.UIThread.CheckAccess()) + { + callback(); + } + else + { + Avalonia.Threading.Dispatcher.UIThread.Post(callback); + } + } +} + +// 使用 +var bootstrap = GeneralUpdateBootstrap.CreateDefault( + options, + eventDispatcher: new AvaloniaEventDispatcher()); +``` + +#### 场景 3:自定义下载器(私有文件服务器) + +【场景说明】企业内部使用私有文件服务器,需要在下载时注入自定义认证头。 + +```csharp +using GeneralUpdate.Avalonia.Android.Abstractions; +using GeneralUpdate.Avalonia.Android.Models; + +public sealed class EnterpriseDownloader : IUpdateDownloader +{ + private readonly HttpClient _httpClient; + private readonly string _enterpriseToken; + + public EnterpriseDownloader(HttpClient httpClient, string enterpriseToken) + { + _httpClient = httpClient; + _enterpriseToken = enterpriseToken; + } + + public async Task DownloadAsync( + UpdatePackageInfo packageInfo, + Action? progressCallback, + CancellationToken cancellationToken = default) + { + var request = new HttpRequestMessage(HttpMethod.Get, packageInfo.DownloadUrl); + request.Headers.Add("X-Enterprise-Token", _enterpriseToken); + + // 自行实现下载和进度报告... + // 返回 DownloadResult + } +} +``` + +--- + +## 5. 常规使用示例 + +### 5.1 快速入门示例(最简 demo) + +```csharp +using GeneralUpdate.Avalonia.Android; +using GeneralUpdate.Avalonia.Android.Models; + +var options = new AndroidUpdateOptions +{ + FileProviderAuthority = "com.myapp.fileprovider" +}; + +var bootstrap = GeneralUpdateBootstrap.CreateDefault(options); + +// 1. 检查更新 +var updatePackage = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "https://update.example.com/app-v2.0.0.apk", + Sha256 = "expected-sha256-hash", + FileSize = 50_000_000, + FileName = "app_update_2.0.0.apk" +}; + +var checkResult = await bootstrap.ValidateAsync( + updatePackage, currentVersion: "1.0.0"); + +if (!checkResult.UpdateFound) +{ + Console.WriteLine("Already up to date."); + return; +} + +// 2. 下载并校验 +var downloadResult = await bootstrap.DownloadAndVerifyAsync(updatePackage); +if (!downloadResult.Success) +{ + Console.WriteLine($"Download failed: {downloadResult.Message}"); + return; +} + +// 3. 安装 +var installResult = await bootstrap.LaunchInstallerAsync( + updatePackage, downloadResult.FilePath!); + +if (installResult.Success) + Console.WriteLine("Installer launched."); +``` + +### 5.2 基础参数组合示例 + +```csharp +using GeneralUpdate.Avalonia.Android; +using GeneralUpdate.Avalonia.Android.Abstractions; +using GeneralUpdate.Avalonia.Android.Models; +using GeneralUpdate.Avalonia.Android.Services; + +var options = new AndroidUpdateOptions +{ + DownloadDirectoryPath = Path.Combine( + Android.App.Application.Context.CacheDir!.AbsolutePath, "myapp-updates"), + FileProviderAuthority = "com.myapp.fileprovider", + TemporaryFileExtension = ".downloading", + DownloadBufferSize = 128 * 1024 // 128 KB buffer +}; + +// 带 HTTP 配置的启动 +var httpOptions = new HttpDownloadOptions +{ + RequestTimeout = TimeSpan.FromSeconds(60), + DownloadTimeout = TimeSpan.FromMinutes(30), + MaxRetryAttempts = 5, + RetryBaseDelay = TimeSpan.FromSeconds(2), + // 开发环境使用自签名证书 + SslValidationPolicy = new AllowAllSslValidationPolicy() +}; + +var bootstrap = GeneralUpdateBootstrap.CreateDefault( + options, + httpOptions: httpOptions); + +// 事件监听 +bootstrap.AddListenerDownloadProgressChanged += (_, e) => +{ + Console.WriteLine( + $"Download: {e.ProgressPercentage:F1}% " + + $"({FormatSize(e.DownloadedBytes)}/{FormatSize(e.TotalBytes)}) " + + $"{FormatSpeed(e.DownloadSpeedBytesPerSecond)}"); +}; + +bootstrap.AddListenerUpdateFailed += (_, e) => +{ + Console.WriteLine($"Failed: {e.Result.Message} (reason: {e.Result.FailureReason})"); +}; + +bootstrap.AddListenerUpdateCompleted += (_, e) => +{ + Console.WriteLine($"Completed: {e.Result.State}"); +}; + +// 执行更新 +var package = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "https://update.example.com/app-v2.0.0.apk", + Sha256 = "expected-sha256-hash", + FileSize = 50_000_000, + FileName = "app-v2.0.0.apk" +}; + +var checkResult = await bootstrap.ValidateAsync(package, "1.0.0"); +if (!checkResult.UpdateFound) return; + +var downloadResult = await bootstrap.DownloadAndVerifyAsync(package); +if (!downloadResult.Success) return; + +await bootstrap.LaunchInstallerAsync(package, downloadResult.FilePath!); + +static string FormatSize(long bytes) => bytes switch +{ + < 1024 => $"{bytes} B", + < 1048576 => $"{bytes / 1024.0:F1} KB", + _ => $"{bytes / 1048576.0:F1} MB" +}; + +static string FormatSpeed(double bytesPerSecond) => + $"{bytesPerSecond / 1048576.0:F1} MB/s"; +``` + +### 5.3 真实业务落地示例 + +完整更新工作流,包含 HTTP 认证、断点续传、权限检查和进度 UI: + +```csharp +using GeneralUpdate.Avalonia.Android; +using GeneralUpdate.Avalonia.Android.Enums; +using GeneralUpdate.Avalonia.Android.Models; + +public sealed class AppUpdateService +{ + private IAndroidBootstrap? _bootstrap; + private bool _disposed; + + public event EventHandler? ProgressChanged; + public event EventHandler? StatusChanged; + + public void Initialize(string fileProviderAuthority, string serverToken) + { + var httpOptions = new HttpDownloadOptions + { + MaxRetryAttempts = 3, + RetryBaseDelay = TimeSpan.FromSeconds(1), + AuthProvider = new Services.BearerTokenAuthProvider(serverToken) + }; + + _bootstrap = GeneralUpdateBootstrap.CreateDefault( + new AndroidUpdateOptions + { + FileProviderAuthority = fileProviderAuthority + }, + httpOptions: httpOptions); + + WireEvents(); + } + + public async Task UpdateAsync( + UpdatePackageInfo package, string currentVersion, + CancellationToken ct = default) + { + if (_bootstrap == null) + throw new InvalidOperationException("Not initialized."); + + // Phase 1: 版本检查 + StatusChanged?.Invoke(this, "Checking for updates..."); + var checkResult = await _bootstrap.ValidateAsync(package, currentVersion, ct); + if (!checkResult.UpdateFound) + { + StatusChanged?.Invoke(this, "Already up to date."); + return true; + } + + StatusChanged?.Invoke(this, $"Update v{package.Version} found."); + + // Phase 2: 下载并校验 + StatusChanged?.Invoke(this, "Downloading..."); + var downloadResult = await _bootstrap.DownloadAndVerifyAsync(package, ct); + if (!downloadResult.Success) + { + StatusChanged?.Invoke(this, + $"Download failed: {downloadResult.Message}"); + return false; + } + + StatusChanged?.Invoke(this, "Download complete, verifying..."); + + // Phase 3: 检查安装权限 (Android 8+) + if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O) + { + var ctx = Android.App.Application.Context; + var pm = ctx.PackageManager; + if (pm != null && !pm.CanRequestPackageInstalls()) + { + StatusChanged?.Invoke(this, + "Please allow install from unknown apps."); + var intent = new Android.Content.Intent( + Android.Provider.Settings.ActionManageUnknownAppSources) + .SetData(Android.Net.Uri.Parse("package:" + ctx.PackageName)); + intent.AddFlags(Android.Content.ActivityFlags.NewTask); + ctx.StartActivity(intent); + return false; + } + } + + // Phase 4: 启动安装 + StatusChanged?.Invoke(this, "Installing..."); + var installResult = await _bootstrap.LaunchInstallerAsync( + package, downloadResult.FilePath!, ct); + + if (installResult.Success) + { + StatusChanged?.Invoke(this, "Installer launched."); + return true; + } + + StatusChanged?.Invoke(this, + $"Install failed: {installResult.Message}"); + return false; + } + + private void WireEvents() + { + if (_bootstrap == null) return; + + _bootstrap.AddListenerDownloadProgressChanged += (_, e) => + { + ProgressChanged?.Invoke(this, e.ProgressPercentage); + }; + + _bootstrap.AddListenerUpdateFailed += (_, e) => + { + StatusChanged?.Invoke(this, + $"Failed: {e.Result.Message} (reason: {e.Result.FailureReason})"); + }; + } + + public void Dispose() + { + if (_disposed) return; + _bootstrap?.Dispose(); + _disposed = true; + } +} +``` + +--- + +## 6. 全局配置 + +### 认证方案 + +Avalonia 组件支持四种认证方案,可在 `UpdatePackageInfo` 单包粒度配置: + +| 认证方案 | AuthScheme | 所需字段 | +| --- | --- | --- | +| HMAC-SHA256 | `Hmac` | `AuthSecretKey` | +| Bearer Token | `Bearer` | `AuthToken` | +| API Key | `ApiKey` | `AuthToken` | +| HTTP Basic | `Basic` | `BasicUsername` + `BasicPassword` | + +也可通过 `HttpDownloadOptions.AuthProvider` 设置全局认证提供器。单包配置优先于全局配置。 + +### SSL 策略 + +| 策略 | 类名 | 使用场景 | +| --- | --- | --- | +| 系统默认(推荐) | `null` | 生产环境 | +| 允许所有(仅开发) | `AllowAllSslValidationPolicy` | 自签名证书开发环境 | +| 自定义 | 实现 `ISslValidationPolicy` | 私有 CA 或证书固定 | + +### 断点续传机制 + +下载器使用临时文件(`.part`)和元数据文件(`.json`)实现断点续传: + +``` +CacheDir/update/ +├── app-v2.0.0.apk.part # 部分下载的临时文件 +├── app-v2.0.0.apk.json # 续传元数据(URL、ETag、预期大小、已下载字节数) +└── app-v2.0.0.apk # 下载完成后的最终文件 +``` + +- 中断后重新下载同一 URL 时,自动读取 `.json` 元数据判断服务端是否支持 Range +- 支持 Range 时从已下载位置继续(`Range: bytes={existingLength}-`) +- 不支持 Range 时重新下载 +- 下载完成后 `.part` 重命名为最终文件名,删除 `.json` 元数据 + +### AndroidManifest 配置 + +使用 FileProvider 分享 APK 文件给 Package Installer,需要在 `AndroidManifest.xml` 中配置: + +```xml + + + + + +``` + +`file_paths.xml`(在 `Resources/xml/` 目录下): + +```xml + + + + +``` + +### 平台差异 + +| 项目 | 说明 | +| --- | --- | +| 最低 API | 26(Android 8.0) | +| 安装权限 | Android 8.0+ 需要 `CanRequestPackageInstalls()` 检查 | +| FileProvider | 必须通过 FileProvider 向系统安装器传递 APK 文件 URI | + +--- + +## 相关资源 + +- [GeneralUpdate.Avalonia 仓库](https://github.com/GeneralLibrary/GeneralUpdate.Avalonia) +- [Android 更新示例](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/UI/AndroidUpdate) +- [Semantic Versioning](https://semver.org/) +- [FileProvider 文档](https://developer.android.com/training/secure-file-sharing/setup-sharing) diff --git a/website/docs/doc/GeneralUpdate.Maui.Android.md b/website/docs/doc/GeneralUpdate.Maui.Android.md new file mode 100644 index 0000000..9f8d0bd --- /dev/null +++ b/website/docs/doc/GeneralUpdate.Maui.Android.md @@ -0,0 +1,738 @@ +--- +sidebar_position: 10 +--- + +# GeneralUpdate.Maui.Android + +**命名空间:** `GeneralUpdate.Maui.Android` | **主要入口:** `GeneralUpdateBootstrap.CreateDefault()` / `AddGeneralUpdateMauiAndroid()` | **NuGet 包:** `GeneralUpdate.Maui.Android` + +## 1. 组件简介 + +### 1.1 组件概述 + +**GeneralUpdate.Maui.Android** 是面向 .NET MAUI 应用的 Android 平台自动更新组件,为 Android APK 提供版本发现、可恢复下载、SHA256 完整性校验以及系统 Package Installer 触发的一站式更新编排。 + +与桌面应用的文件替换式更新不同,Android APK 更新必须经由系统 Package Installer 完成。该组件封装了这一平台差异,并提供两阶段 API:`ValidateAsync` 做版本检查,`ExecuteUpdateAsync` 串起下载→校验→安装的完整流程。 + +**核心能力:** + +| 能力 | 说明 | +| --- | --- | +| 版本对比检查 | 内置 `System.Version` 解析,自动判断当前版本是否低于目标版本 | +| 可恢复 APK 下载 | `HttpRangeDownloader` 基于 HTTP Range 请求头实现断点续传,支持进度报告 | +| SHA256 完整性校验 | 下载后自动计算文件 SHA256 与服务端哈希比对,损坏文件自动删除 | +| 系统安装器触发 | `AndroidApkInstaller` 通过 FileProvider + Intent 启动系统 Package Installer | +| DI 容器集成 | `AddGeneralUpdateMauiAndroid()` 扩展方法一键注册所有服务到 `IServiceCollection` | +| HTTP 传输配置 | `HttpDownloadOptions` 支持 SSL 证书策略、代理、超时、认证,支持指数退避重试 | +| 多协议认证 | 支持 HMAC-SHA256、Bearer Token、API Key、HTTP Basic 四种认证方案 | +| 事件通知 | 版本发现、下载进度、完成阶段、失败原因等全方位事件 | +| 并发安全 | `ExecuteUpdateAsync` 内置 `Interlocked` 并发保护,重复调用直接返回 `AlreadyInProgress` | +| 临时文件管理 | 下载使用 `.downloading` 临时扩展名,完成后原子替换为目标文件 | + +**解决的业务痛点:** +- MAUI Android 应用需要内置自动更新能力,但 APK 安装必须绕过 Google Play 通过系统安装器完成 +- 大 APK 下载过程中网络不稳定,需要断点续传减少用户流量消耗 +- 需要确保下载的 APK 未被篡改或损坏,保障更新安全 +- 需要与 MAUI 的 DI 容器深度集成,简化接入代码 + +:::info MAUI 更新 ≠ 桌面文件替换 +Android 平台无法像桌面端那样直接替换可执行文件。APK 必须通过 Android Package Installer 安装,系统会处理签名验证和应用替换。MAUI 组件封装了这一差异,但 **APK 签名、版本号递增** 仍然需要你在构建流水线中保证。 +::: + +**业务使用场景:** +- MAUI Android 应用的自动更新(企业内部分发、Beta 测试、旁加载) +- 需要通过 CDN / OSS 分发 APK 包的应用 +- 需要 DI 容器管理的 MAUI 应用 + +### 1.2 环境与依赖 + +| 项目 | 说明 | +| --- | --- | +| **NuGet 包** | `GeneralUpdate.Maui.Android` | +| **目标框架** | `net10.0` + `net10.0-android`(多目标;Android 最低 API 21) | +| **依赖包** | `Microsoft.Extensions.DependencyInjection.Abstractions`、`Microsoft.Extensions.Http`、`Xamarin.AndroidX.Core` | +| **兼容性** | Android 5.0 (API 21) 及以上版本 | + +--- + +## 2. 组件功能列表 + +| 功能名称 | 功能描述 | 类型 | 是否必填 | 备注限制 | +| --- | --- | --- | --- | --- | +| 版本对比检查 | 对比当前版本与目标版本,判断是否有更新可用 | 基础 | 必选 | `ValidateAsync`,基于 `System.Version` 解析 | +| 更新执行 | 串起下载→校验→安装全流程 | 基础 | 必选 | `ExecuteUpdateAsync`,并发保护 | +| APK 可恢复下载 | HTTP Range 断点续传 + 进度报告 | 基础 | 自动 | `HttpRangeDownloader` | +| SHA256 校验 | 下载后自动校验,损坏文件自动删除 | 基础 | 自动 | 通过 `IHashValidator`,支持进度回调 | +| APK 安装触发 | FileProvider + Intent 启动系统安装器 | 基础 | 自动 | `AndroidApkInstaller`,自动检查安装权限 | +| DI 容器集成 | 一键注册所有更新服务 | 拓展 | 推荐 | `AddGeneralUpdateMauiAndroid()` | +| HTTP 传输配置 | SSL、代理、超时、重试、认证 | 拓展 | 可选 | `HttpDownloadOptions` | +| 多协议认证 | HMAC-SHA256 / Bearer / API Key / Basic | 拓展 | 可选 | 全局或单包粒度配置 | +| 进度通知 | 下载字节数、速度、百分比、剩余时间 | 基础 | 可选 | `IProgress` 回调 | +| 完成通知 | 各完成阶段(下载完成、校验完成、安装触发、工作流完成) | 基础 | 可选 | `AddListenerUpdateCompleted` | +| 失败通知 | 失败原因分类、异常信息 | 基础 | 可选 | `AddListenerUpdateFailed` | +| 验证通知 | 发现可用更新时触发 | 基础 | 可选 | `AddListenerValidate` | +| 状态查询 | 实时查询当前更新状态 | 基础 | 可选 | `CurrentState` 属性 | +| 并发保护 | 防止重复执行更新操作 | 基础 | 自动 | `Interlocked` 原子操作 | +| 损坏文件自动清理 | 校验失败自动删除损坏包 | 拓展 | 可选 | `UpdateOptions.DeleteCorruptedPackageOnFailure` | +| 进度报告间隔控制 | 自定义进度回调频率 | 拓展 | 可选 | `UpdateOptions.ProgressReportInterval` | +| 自定义下载器 | 自定义下载实现 | 拓展 | 可选 | 实现 `IUpdateDownloader` | +| 自定义哈希验证 | 自定义哈希实现 | 拓展 | 可选 | 实现 `IHashValidator` | +| 自定义安装器 | 自定义安装逻辑 | 拓展 | 可选 | 实现 `IApkInstaller` | +| 自定义存储提供器 | 自定义文件路径和存储操作 | 拓展 | 可选 | 实现 `IUpdateStorageProvider` | +| 自定义日志 | 自定义日志输出 | 拓展 | 可选 | 实现 `IUpdateLogger` | +| 自定义 SSL 策略 | 自定义 HTTPS 证书校验 | 拓展 | 可选 | 实现 `ISslValidationPolicy` | +| 自定义 HTTP 认证 | 自定义 HTTP 请求认证 | 拓展 | 可选 | 实现 `IHttpAuthProvider` | + +--- + +## 3. API 配置说明 + +### 3.1 配置字段(属性 Props) + +**UpdateOptions:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 说明 | +| --- | --- | --- | --- | --- | +| `CurrentVersion` | `string` | `""` | **是** | 当前应用版本号 | +| `DownloadDirectory` | `string?` | `null` | 可选 | 下载目录,为空时自动选择 | +| `TemporaryFileExtension` | `string` | `".downloading"` | 可选 | 临时下载文件扩展名 | +| `DeleteCorruptedPackageOnFailure` | `bool` | `true` | 可选 | 校验失败时自动删除损坏包 | +| `ProgressReportInterval` | `TimeSpan` | `500ms` | 可选 | 进度报告间隔 | +| `InstallOptions` | `AndroidInstallOptions` | `new()` | 可选 | 安装选项 | + +**AndroidInstallOptions:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 说明 | +| --- | --- | --- | --- | --- | +| `FileProviderAuthority` | `string` | `""` | **是** | FileProvider authority,需与 AndroidManifest 一致 | +| `MimeType` | `string` | `"application/vnd.android.package-archive"` | 可选 | APK 文件的 MIME 类型 | + +**HttpDownloadOptions:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 说明 | +| --- | --- | --- | --- | --- | +| `SslValidationPolicy` | `ISslValidationPolicy?` | `null` | 可选 | 自定义 SSL 证书验证策略 | +| `DownloadTimeout` | `TimeSpan` | `10min` | 可选 | 整体下载超时 | +| `Proxy` | `IWebProxy?` | `null` | 可选 | HTTP 代理 | +| `UseProxy` | `bool` | `false` | 可选 | 是否启用代理 | +| `AuthProvider` | `IHttpAuthProvider?` | `null` | 可选 | 全局 HTTP 认证提供器 | + +**UpdatePackageInfo:** + +| 字段名 | 数据类型 | 默认值 | 是否必填 | 说明 | +| --- | --- | --- | --- | --- | +| `Version` | `string` | `""` | **是** | 目标版本号 | +| `DownloadUrl` | `string` | `""` | **是** | APK 下载地址 | +| `Sha256` | `string` | `""` | **是** | SHA256 哈希值 | +| `PackageSize` | `long?` | `null` | 可选 | 包大小(字节) | +| `ApkFileName` | `string?` | `null` | 可选 | APK 文件名 | +| `ForceUpdate` | `bool` | `false` | 可选 | 是否强制更新 | +| `VersionName` | `string?` | `null` | 可选 | 展示名称 | +| `ReleaseNotes` | `string?` | `null` | 可选 | 发布说明 | +| `PublishTime` | `DateTimeOffset?` | `null` | 可选 | 发布时间 | +| `AuthScheme` | `AuthScheme?` | `null` | 可选 | 认证方案 | +| `AuthToken` | `string?` | `null` | 可选 | 认证令牌 | +| `AuthSecretKey` | `string?` | `null` | 可选 | HMAC 密钥 | +| `BasicUsername` | `string?` | `null` | 可选 | Basic 用户名 | +| `BasicPassword` | `string?` | `null` | 可选 | Basic 密码 | + +**UpdateState 枚举:** + +| 枚举值 | 说明 | +| --- | --- | +| `None` | 初始/空闲状态 | +| `Checking` | 正在检查更新 | +| `UpdateAvailable` | 发现可用更新 | +| `Downloading` | 正在下载 | +| `Verifying` | 正在校验 | +| `ReadyToInstall` | 准备安装 | +| `Installing` | 正在安装 | +| `Completed` | 更新完成 | +| `Failed` | 更新失败 | +| `Canceled` | 已取消 | + +**UpdateFailureReason 枚举:** + +| 枚举值 | 说明 | +| --- | --- | +| `Unknown` | 未知错误 | +| `InvalidInput` | 输入参数无效 | +| `AlreadyInProgress` | 已有更新操作正在进行 | +| `Network` | 网络错误 | +| `Download` | 下载错误 | +| `FileAccess` | 文件访问错误 | +| `IntegrityCheckFailed` | 完整性校验失败 | +| `InstallPermissionDenied` | 安装权限被拒绝 | +| `Installation` | 安装过程错误 | +| `Canceled` | 用户取消 | +| `NoUpdateAvailable` | 无可用更新 | + +**UpdateCompletionStage 枚举:** + +| 枚举值 | 说明 | +| --- | --- | +| `DownloadCompleted` | 下载完成 | +| `VerificationCompleted` | 校验完成 | +| `InstallationTriggered` | 安装器已启动 | +| `WorkflowCompleted` | 工作流完成 | + +### 3.2 实例方法 + +**GeneralUpdateBootstrap(静态工厂):** + +| 方法名 | 入参明细 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | +| `CreateDefault(HttpClient?, IUpdateLogger?, HttpDownloadOptions?)` | `httpClient` — 可选外部 HttpClient;`logger` — 可选日志器;`httpOptions` — 可选 HTTP 配置 | 创建默认的 Android 更新引导实例 | `httpOptions` 生效时 `httpClient` 参数被忽略 | +| `AddGeneralUpdateMauiAndroid(IServiceCollection, HttpClient?)` | `services` — DI 容器;`httpClient` — 可选外部 HttpClient | 在 MAUI DI 容器中注册所有更新服务 | 返回 `IServiceCollection` 支持链式调用 | + +**IAndroidBootstrap:** + +| 方法名 | 入参明细 | 使用场景 | 注意事项 | +| --- | --- | --- | --- | +| `ValidateAsync(UpdatePackageInfo, UpdateOptions, CancellationToken)` | `packageInfo` — 更新包信息;`options` — 更新选项;`ct` — 取消令牌 | 检查是否有可用的更新 | 返回 `UpdateCheckResult`,包含 `IsUpdateAvailable` | +| `ExecuteUpdateAsync(UpdatePackageInfo, UpdateOptions, CancellationToken)` | `packageInfo` — 更新包信息;`options` — 更新选项;`ct` — 取消令牌 | 执行完整更新:下载→校验→安装 | 内置并发保护,重复调用返回 `AlreadyInProgress` | +| `Dispose()` | 无 | 释放下载器等资源 | — | +| `CurrentState` | 属性(`UpdateState`) | 查询当前更新状态 | 线程安全(`Volatile.Read`) | + +### 3.3 回调事件 + +| 事件名称 | 回调参数 | 触发时机 | 使用说明 | +| --- | --- | --- | --- | +| `AddListenerValidate` | `ValidateEventArgs` — `PackageInfo` | 版本对比完成,发现可用更新时 | 可用于 UI 展示新版本信息 | +| `AddListenerDownloadProgressChanged` | `DownloadProgressChangedEventArgs` — `PackageInfo`, `Statistics`(`DownloadProgressInfo`:`ProgressPercentage`, `DownloadedBytes`, `TotalBytes`, `BytesPerSecond`, `RemainingBytes`), `StatusDescription` | 下载进度更新时 | 通过 `IProgress` 回调 | +| `AddListenerUpdateCompleted` | `UpdateCompletedEventArgs` — `PackageInfo`, `Stage`(`UpdateCompletionStage`), `PackagePath` | 各阶段完成时 | 分为下载完成、校验完成、安装触发、工作流完成四个阶段 | +| `AddListenerUpdateFailed` | `UpdateFailedEventArgs` — `Reason`, `Message`, `Exception`, `PackageInfo` | 任何阶段失败时 | 包含结构化的失败原因 | + +--- + +## 4. 扩展示例(高阶用法) + +### 4.1 组件可扩展能力总览 + +所有服务可通过 `AddGeneralUpdateMauiAndroid()` 注册后,通过 DI 容器替换: + +| 接口 | 默认实现 | 说明 | +| --- | --- | --- | +| `IUpdateDownloader` | `HttpRangeDownloader` | 可恢复 APK 下载 | +| `IHashValidator` | `Sha256Validator` | SHA256 哈希校验 | +| `IApkInstaller` | `AndroidApkInstaller` | APK 安装触发 | +| `IUpdateStorageProvider` | `UpdateFileStore` | 文件路径和存储操作 | +| `IUpdateLogger` | `NullUpdateLogger` | 日志记录器 | +| `IUpdateBootstrap` | `AndroidBootstrap` | 更新编排(通过 DI 注册) | +| `ISslValidationPolicy` | 通过 `HttpDownloadOptions` 配置 | SSL 证书验证策略 | +| `IHttpAuthProvider` | 通过 `HttpDownloadOptions` 或单包配置 | HTTP 认证提供器 | + +### 4.2 分场景示例 + +#### 场景 1:DI 容器集成 + +【场景说明】在 MAUI 应用中将更新服务注册到 DI 容器,便于管理和测试。 + +```csharp +using GeneralUpdate.Maui.Android.Abstractions; +using GeneralUpdate.Maui.Android.Models; +using GeneralUpdate.Maui.Android.Services; +using Microsoft.Extensions.Logging; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => { }); + + // 注册更新服务 + builder.Services.AddGeneralUpdateMauiAndroid(); + + // 注册应用服务 + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + return builder.Build(); + } +} + +// 在 ViewModel 中使用 +public class MainViewModel +{ + private readonly IAndroidBootstrap _bootstrap; + + public MainViewModel(IAndroidBootstrap bootstrap) + { + _bootstrap = bootstrap; + WireEvents(); + } + + public async Task CheckAndUpdateAsync() + { + var package = new UpdatePackageInfo + { + Version = "2.0.0", + DownloadUrl = "https://example.com/app-v2.apk", + Sha256 = "expected-sha256" + }; + + var options = new UpdateOptions + { + CurrentVersion = "1.0.0", + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = "com.myapp.fileprovider" + } + }; + + var result = await _bootstrap.ExecuteUpdateAsync(package, options, CancellationToken.None); + Console.WriteLine(result.IsSuccess ? "Update succeeded!" : $"Failed: {result.Message}"); + } + + private void WireEvents() + { + _bootstrap.AddListenerDownloadProgressChanged += (_, args) => + { + Console.WriteLine($"Progress: {args.Statistics.ProgressPercentage:F1}%"); + }; + } +} +``` + +#### 场景 2:自定义 HTTP 认证 + +【场景说明】企业内部分发的 APK 需要 Bearer Token 认证。 + +```csharp +using GeneralUpdate.Maui.Android.Abstractions; +using GeneralUpdate.Maui.Android.Models; +using GeneralUpdate.Maui.Android.Services; + +// 方式一:使用全局认证提供器 +var httpOptions = new HttpDownloadOptions +{ + AuthProvider = new Services.BearerTokenAuthProvider("your-token-here") +}; + +var bootstrap = GeneralUpdateBootstrap.CreateDefault( + httpClient: null, + logger: null, + httpOptions: httpOptions); + +// 方式二:使用单包认证 +var package = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "https://example.com/app-v2.apk", + Sha256 = "expected-sha256", + AuthScheme = Enums.AuthScheme.Bearer, + AuthToken = "your-token-here" +}; +``` + +#### 场景 3:非容器环境下的直接使用 + +【场景说明】简单应用不依赖 DI 容器,直接使用静态工厂创建。 + +```csharp +using GeneralUpdate.Maui.Android; +using GeneralUpdate.Maui.Android.Models; + +// 直接创建 +var bootstrap = GeneralUpdateBootstrap.CreateDefault(); + +// 配置更新选项 +var options = new UpdateOptions +{ + CurrentVersion = "1.0.0", + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = "com.myapp.fileprovider" + } +}; + +var package = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "https://example.com/app-v2.apk", + Sha256 = "expected-sha256", + PackageSize = 50_000_000, + ApkFileName = "app-v2.apk" +}; + +// 执行更新 +var result = await bootstrap.ExecuteUpdateAsync(package, options, CancellationToken.None); +``` + +--- + +## 5. 常规使用示例 + +### 5.1 快速入门示例(最简 demo) + +```csharp +using GeneralUpdate.Maui.Android; +using GeneralUpdate.Maui.Android.Models; + +// 创建实例 +var bootstrap = GeneralUpdateBootstrap.CreateDefault(); + +// 更新包信息(通常从服务端 API 获取) +var package = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "https://update.example.com/app-v2.0.0.apk", + Sha256 = "a1b2c3d4e5f6..." // 服务端返回的 SHA256 +}; + +// 更新选项 +var options = new UpdateOptions +{ + CurrentVersion = "1.0.0", + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = "com.myapp.fileprovider" + } +}; + +// 检查更新 +var checkResult = await bootstrap.ValidateAsync(package, options, CancellationToken.None); +if (!checkResult.IsUpdateAvailable) +{ + Console.WriteLine("Already up to date."); + return; +} + +// 执行更新(下载 → 校验 → 安装) +var result = await bootstrap.ExecuteUpdateAsync(package, options, CancellationToken.None); +Console.WriteLine(result.IsSuccess ? "Update completed." : $"Update failed: {result.Message}"); +``` + +### 5.2 基础参数组合示例 + +```csharp +using GeneralUpdate.Maui.Android; +using GeneralUpdate.Maui.Android.Enums; +using GeneralUpdate.Maui.Android.Models; + +var httpOptions = new HttpDownloadOptions +{ + DownloadTimeout = TimeSpan.FromMinutes(30), + AuthProvider = new GeneralUpdate.Maui.Android.Services.BearerTokenAuthProvider("token") +}; + +var bootstrap = GeneralUpdateBootstrap.CreateDefault( + httpClient: null, + logger: null, + httpOptions: httpOptions); + +// 事件监听 +bootstrap.AddListenerDownloadProgressChanged += (_, e) => +{ + var s = e.Statistics; + Console.WriteLine($"Progress: {s.ProgressPercentage:F1}% | " + + $"{FormatSize(s.DownloadedBytes)}/{FormatSize(s.TotalBytes)} | " + + $"{FormatSpeed(s.BytesPerSecond)}"); +}; + +bootstrap.AddListenerUpdateCompleted += (_, e) => +{ + Console.WriteLine($"Stage completed: {e.Stage}"); +}; + +bootstrap.AddListenerUpdateFailed += (_, e) => +{ + Console.WriteLine($"Failed: {e.Reason} — {e.Message}"); +}; + +var options = new UpdateOptions +{ + CurrentVersion = "1.0.0", + TemporaryFileExtension = ".downloading", + DeleteCorruptedPackageOnFailure = true, + ProgressReportInterval = TimeSpan.FromMilliseconds(250), + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = "com.myapp.fileprovider" + } +}; + +var package = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "https://cdn.example.com/app-v2.0.0.apk", + Sha256 = "expected-sha256", + PackageSize = 50_000_000, + ApkFileName = "app-v2.0.0.apk", + ForceUpdate = false, + ReleaseNotes = "Bug fixes and performance improvements" +}; + +var result = await bootstrap.ExecuteUpdateAsync(package, options, CancellationToken.None); + +if (!result.IsSuccess) +{ + Console.WriteLine($"Update failed: {result.FailureReason}"); +} + +// 辅助方法 +static string FormatSize(long bytes) => bytes switch +{ + < 1024 => $"{bytes} B", + < 1048576 => $"{bytes / 1024.0:F1} KB", + _ => $"{bytes / 1048576.0:F1} MB" +}; + +static string FormatSpeed(double bytesPerSecond) => + $"{bytesPerSecond / 1048576.0:F1} MB/s"; +``` + +### 5.3 真实业务落地示例 + +完整 MAUI Android 更新工作流,包含服务端 API 对接、事件处理和错误分类: + +```csharp +using GeneralUpdate.Maui.Android; +using GeneralUpdate.Maui.Android.Abstractions; +using GeneralUpdate.Maui.Android.Enums; +using GeneralUpdate.Maui.Android.Models; +using System.Net.Http.Json; + +public sealed class MauiUpdateService : IDisposable +{ + private readonly HttpClient _httpClient; + private IAndroidBootstrap? _bootstrap; + private readonly string _fileProviderAuthority; + private bool _disposed; + + public MauiUpdateService(HttpClient httpClient, string fileProviderAuthority) + { + _httpClient = httpClient; + _fileProviderAuthority = fileProviderAuthority; + } + + // 初始化(接收 HttpDownloadOptions 实现可选认证) + public void Initialize(HttpDownloadOptions? httpOptions = null) + { + _bootstrap = GeneralUpdateBootstrap.CreateDefault( + httpClient: null, + logger: null, + httpOptions: httpOptions); + WireEvents(); + } + + // 从服务端获取更新包信息并执行更新 + public async Task CheckAndUpdateAsync( + string serverUrl, string currentVersion, + CancellationToken ct = default) + { + if (_bootstrap == null) + throw new InvalidOperationException("Call Initialize() first."); + + try + { + // 1. 从服务端获取更新包信息 + var package = await FetchPackageFromServerAsync(serverUrl, currentVersion, ct); + if (package == null) + return UpdateResult.NoUpdate(); + + // 2. 检查版本 + var options = new UpdateOptions + { + CurrentVersion = currentVersion, + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = _fileProviderAuthority + } + }; + + var checkResult = await _bootstrap.ValidateAsync(package, options, ct); + if (!checkResult.IsUpdateAvailable) + return UpdateResult.NoUpdate(); + + // 3. 执行更新 + var execResult = await _bootstrap.ExecuteUpdateAsync(package, options, ct); + + return execResult.IsSuccess + ? UpdateResult.Success() + : UpdateResult.Failure(execResult.FailureReason, execResult.Message); + } + catch (OperationCanceledException) + { + return UpdateResult.Canceled(); + } + catch (HttpRequestException ex) + { + return UpdateResult.Failure(UpdateFailureReason.Network, ex.Message); + } + } + + private async Task FetchPackageFromServerAsync( + string serverUrl, string currentVersion, CancellationToken ct) + { + // 调用服务端版本检查 API + var request = new + { + Version = currentVersion, + AppType = 1, + Platform = 4, + ProductId = "your-product-id" + }; + + var response = await _httpClient.PostAsJsonAsync( + $"{serverUrl}/Upgrade/Verification", request, ct); + + response.EnsureSuccessStatusCode(); + + // 解析响应体 + var body = await response.Content.ReadFromJsonAsync(ct); + // ... 映射到 UpdatePackageInfo + + return null; // 实际开发中替换为真实映射逻辑 + } + + private void WireEvents() + { + if (_bootstrap == null) return; + + _bootstrap.AddListenerDownloadProgressChanged += (_, e) => + { + var s = e.Statistics; + OnProgressChanged?.Invoke(this, + (s.ProgressPercentage, s.BytesPerSecond)); + }; + + _bootstrap.AddListenerUpdateFailed += (_, e) => + { + OnStatusChanged?.Invoke(this, + $"Failed [{e.Reason}]: {e.Message}"); + }; + + _bootstrap.AddListenerUpdateCompleted += (_, e) => + { + OnStatusChanged?.Invoke(this, $"Stage: {e.Stage}"); + }; + } + + // 公开事件 + public event EventHandler<(double Percentage, double Speed)>? OnProgressChanged; + public event EventHandler? OnStatusChanged; + + public void Dispose() + { + if (_disposed) return; + _bootstrap?.Dispose(); + _disposed = true; + } +} + +// 结果模型 +public sealed record UpdateResult +{ + public bool IsSuccess { get; init; } + public bool IsCanceled { get; init; } + public UpdateFailureReason FailureReason { get; init; } + public string? Message { get; init; } + + public static UpdateResult Success() => new() { IsSuccess = true }; + public static UpdateResult NoUpdate() => new() { IsSuccess = true, Message = "No update available." }; + public static UpdateResult Canceled() => new() { IsCanceled = true, Message = "Canceled." }; + public static UpdateResult Failure(UpdateFailureReason reason, string message) => + new() { FailureReason = reason, Message = message }; +} + +// 注意:此为示例结构,实际需要替换为服务端返回的格式 +public sealed record ServerResponse +{ + public int Code { get; init; } + public List? Body { get; init; } +} + +public sealed record PackageEntry +{ + public string Version { get; init; } = ""; + public string Url { get; init; } = ""; + public string Hash { get; init; } = ""; + public long Size { get; init; } +} +``` + +--- + +## 6. 全局配置 + +### DI 注册方式 + +```csharp +// 方式一:使用默认 HttpClient +services.AddGeneralUpdateMauiAndroid(); + +// 方式二:外部传入 HttpClient(共享连接池) +services.AddGeneralUpdateMauiAndroid(myHttpClient); + +// 方式三:IHttpClientFactory 注入(通过 AddHttpClient) +services.AddHttpClient(client => +{ + client.Timeout = TimeSpan.FromMinutes(10); +}); +services.AddSingleton(); +// 其他服务同理... +``` + +### 认证方案 + +MAUI 组件支持四种认证方案,可在 `UpdatePackageInfo` 单包粒度配置: + +| 认证方案 | AuthScheme | 所需字段 | +| --- | --- | --- | +| HMAC-SHA256 | `Hmac` | `AuthSecretKey` | +| Bearer Token | `Bearer` | `AuthToken` | +| API Key | `ApiKey` | `AuthToken` | +| HTTP Basic | `Basic` | `BasicUsername` + `BasicPassword` | + +### SSL 策略 + +| 策略 | 类名 | 使用场景 | +| --- | --- | --- | +| 系统默认(推荐) | `null` | 生产环境 | +| 允许所有(仅开发) | `AllowAllSslValidationPolicy` | 自签名证书开发环境 | +| 自定义 | 实现 `ISslValidationPolicy` | 私有 CA | + +### AndroidManifest 配置 + +```xml + + + + + +``` + +`file_paths.xml`(`Platforms/Android/Resources/xml/file_paths.xml`): + +```xml + + + + +``` + +### 平台差异 + +| 项目 | 说明 | +| --- | --- | +| 最低 API | 21(Android 5.0) | +| 安装权限 | Android 8.0+ 需要 `CanRequestPackageInstalls()` 检查 | +| FileProvider | 必须通过 FileProvider 向系统安装器传递 APK 文件 URI | + +--- + +## 相关资源 + +- [GeneralUpdate.Maui 仓库](https://github.com/GeneralLibrary/GeneralUpdate.Maui) +- [MAUI Android 更新示例](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/UI/MauiUpdate) +- [FileProvider 文档](https://developer.android.com/training/secure-file-sharing/setup-sharing) diff --git a/website/docs/doc/_category_.json b/website/docs/doc/_category_.json index 5af93ba..67be6c6 100644 --- a/website/docs/doc/_category_.json +++ b/website/docs/doc/_category_.json @@ -3,6 +3,6 @@ "position": 2, "link": { "type": "generated-index", - "description": "GeneralUpdate 组件参考文档 — 涵盖 Core、Bowl、Differential、Drivelution 与 Extension 五大非固件组件。" + "description": "GeneralUpdate 组件参考文档 — 涵盖 Core、Bowl、Differential、Drivelution、Extension、Avalonia.Android 与 Maui.Android 七大非固件组件。" } } diff --git a/website/docs/quickstart/Avalonia Android cookbook.md b/website/docs/quickstart/Avalonia Android cookbook.md new file mode 100644 index 0000000..08a19db --- /dev/null +++ b/website/docs/quickstart/Avalonia Android cookbook.md @@ -0,0 +1,202 @@ +--- +sidebar_position: 4 +title: Avalonia Android 更新入门 +--- + +# GeneralUpdate.Avalonia.Android 快速入门 + +这篇手册面向需要在 Avalonia 应用中集成 Android APK 自动更新的开发者。目标是用最少的代码跑通"检查更新 → 下载 APK → 校验 SHA256 → 启动安装器"的完整流程。 + +:::info 前置知识 +这篇手册假设你已经有一个 Avalonia 项目并配置好了 Android 目标框架。如果你还没有 Avalonia Android 项目,请先参考 [Avalonia 官方文档](https://docs.avaloniaui.net/) 创建。 +::: + +## 更新流程 + +``` +① 检查版本 ② 下载 APK ③ 安装 +┌──────────┐ ┌──────────┐ ┌──────────────┐ +│ Client │──POST──→ │ Server │ │ Android │ +│(Avalonia)│←─JSON─── │(更新服务)│ │ Package │ +└────┬─────┘ └────┬─────┘ │ Installer │ + │ │ └──────┬───────┘ + │ 服务器返回包信息 │ │ + │ ←────────────────────│ │ + │ │ │ + │ GET /packages/app-v2.0.0.apk │ + │ ────────────────────→│ │ + │ APK 文件 (支持 Range)│ │ + │ ←────────────────────│ │ + │ │ │ + │ SHA256 校验通过后 │ │ + │ 启动 Package Installer ─────────────────→ │ + │ │ │ +``` + +| 角色 | 定义 | 负责什么 | +| --- | --- | --- | +| Client(Avalonia) | 你的 Avalonia Android 应用 | 检查版本 → 下载 APK → 校验 → 启动安装器 | +| Server | 更新服务(如 Samples 中的 Server) | 返回版本信息、提供 APK 下载 | +| Android Package Installer | Android 系统组件 | 安装 APK(验证签名、替换应用) | + +## Phase 1:环境准备 + +### 安装清单 + +| 项 | 要求 | 验证命令 | +| --- | --- | --- | +| .NET SDK | 10.0+ | `dotnet --version` | +| Android SDK | API 26+ | `dotnet workload list`(应包含 `android`) | + +### NuGet 包 + +```bash +dotnet add package GeneralUpdate.Avalonia.Android +``` + +## Phase 2:FileProvider 配置 + +APK 文件需要通过 FileProvider 传递给 Android Package Installer。在 `AndroidManifest.xml` 中添加: + +```xml + + + + + +``` + +创建 `Resources/xml/file_paths.xml`: + +```xml + + + + +``` + +> **注意**:`authorities` 中的 `${applicationId}.fileprovider` 和代码中传入的 `FileProviderAuthority` 必须一致。 + +## Phase 3:编写更新代码 + +在 Avalonia ViewModel 或 Service 中集成更新逻辑: + +```csharp +using GeneralUpdate.Avalonia.Android; +using GeneralUpdate.Avalonia.Android.Models; +using GeneralUpdate.Avalonia.Android.Abstractions; + +public class UpdateViewModel : ViewModelBase +{ + private IAndroidBootstrap? _bootstrap; + + public async Task CheckAndUpdateAsync() + { + // 1. 创建更新引导实例 + var options = new AndroidUpdateOptions + { + FileProviderAuthority = "com.myapp.fileprovider" + }; + + _bootstrap = GeneralUpdateBootstrap.CreateDefault(options); + + // 2. 订阅事件 + _bootstrap.AddListenerDownloadProgressChanged += (_, e) => + { + // 更新 UI 进度条 + Console.WriteLine($"Download: {e.ProgressPercentage:F1}%"); + }; + + // 3. 构造更新包信息(通常从服务端获取) + var package = new UpdatePackageInfo + { + Version = "2.0.0", + DownloadUrl = "http://10.0.2.2:5000/packages/app-v2.0.0.apk", + Sha256 = "服务端返回的SHA256哈希值", + FileSize = 50_000_000, + FileName = "app-v2.0.0.apk" + }; + + // 4. 检查更新 + var checkResult = await _bootstrap.ValidateAsync(package, "1.0.0"); + if (!checkResult.UpdateFound) + { + Console.WriteLine("已是最新版本"); + return; + } + + // 5. 下载并校验 + var downloadResult = await _bootstrap.DownloadAndVerifyAsync(package); + if (!downloadResult.Success) + { + Console.WriteLine($"下载失败: {downloadResult.Message}"); + return; + } + + // 6. 检查安装权限 (Android 8+) + if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O) + { + var ctx = Android.App.Application.Context; + var pm = ctx.PackageManager; + if (pm != null && !pm.CanRequestPackageInstalls()) + { + // 引导用户开启"安装未知应用"权限 + var intent = new Android.Content.Intent( + Android.Provider.Settings.ActionManageUnknownAppSources) + .SetData(Android.Net.Uri.Parse("package:" + ctx.PackageName)); + intent.AddFlags(Android.Content.ActivityFlags.NewTask); + ctx.StartActivity(intent); + return; + } + } + + // 7. 启动安装器 + var installResult = await _bootstrap.LaunchInstallerAsync( + package, downloadResult.FilePath!); + + if (installResult.Success) + Console.WriteLine("安装器已启动,请在设备上确认安装。"); + } +} +``` + +## Phase 4:启动 Server + +使用 Samples 仓库中的 Server: + +```bash +git clone https://github.com/GeneralLibrary/GeneralUpdate-Samples.git +cd GeneralUpdate-Samples/src/Server +dotnet run +``` + +Server 默认在 `http://localhost:5000` 启动。Android 模拟器中请使用 `http://10.0.2.2:5000` 访问宿主机的 localhost。 + +## Phase 5:端到端验证 + +1. 将 APK 包放到 `Server/wwwroot/packages/` 目录 +2. 更新 `wwwroot/packages/versions.json` 添加版本信息 +3. 在模拟器或真机上运行 Avalonia Android 应用 +4. 观察控制台输出:检查更新 → 下载 → 校验 → 启动安装器 + +### 预期输出 + +``` +Checking for updates... +Update v2.0.0 found. +Downloading... 45% 1.2MB/s +Downloading... 100% +SHA256 verified. +Installer launched. +``` + +## 下一步 + +- [GeneralUpdate.Avalonia.Android 组件文档](../doc/GeneralUpdate.Avalonia.Android):完整 API 参考、配置说明和高级用法 +- [Android 更新示例](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/UI/AndroidUpdate):完整的 Avalonia Android 更新示例项目 diff --git a/website/docs/quickstart/GeneralUpdate.PacketTool.md b/website/docs/quickstart/GeneralUpdate.PacketTool.md index b823ea3..37e8511 100644 --- a/website/docs/quickstart/GeneralUpdate.PacketTool.md +++ b/website/docs/quickstart/GeneralUpdate.PacketTool.md @@ -32,7 +32,7 @@ dotnet run --project GeneralUpdate.Tools.csproj > Simulation 模块内部会调用 `dotnet publish` 构建测试应用,因此使用仿真功能时必须安装 .NET SDK,仅运行 Patch / Extension / OSS / Config 模块则不需要。 -## 六个模块速览 +## 七个模块速览 | 模块 | 你提供 | 工具产出 | 下游消费者 | |------|--------|----------|------------| @@ -42,6 +42,7 @@ dotnet run --project GeneralUpdate.Tools.csproj | **Config** | Client/Upgrade 的 `.csproj` | `generalupdate.manifest.json` + `sample_output/` 发布目录 | Client/Upgrade 启动引导 | | **Simulation** | 旧版本目录 + 补丁 ZIP | 本地更新服务 + `simulation_report.md` | 发布前质量把关 | | **Hash** | 本地文件(ZIP) | SHA256 小写十六进制字符串 | 完整性校验、服务端版本记录 | +| **Mobile** | APK/AAB 文件或 MAUI/Avalonia Android 的 `.csproj` | `mobile_version_{timestamp}.json` 版本记录 | 移动端更新服务端、GeneralUpdate.Avalonia/Maui 组件 | --- @@ -325,9 +326,103 @@ Generate Sample 额外输出: --- +## Mobile:移动端打包 + +### 解决什么问题 + +如果你使用 `GeneralUpdate.Avalonia.Android` 或 `GeneralUpdate.Maui.Android` 组件为 Android 应用提供自动更新能力,每次发布新版本时都需要获取 APK/AAB 文件的元数据(包名、版本号、SHA256 哈希、文件大小),并将其上传到服务端进行版本管理。Mobile 模块把"解析 → 打包 → 上传 → 生成版本记录"这四步收敛到一个界面中。 + +支持两种工作模式: + +- **文件模式**:直接选择 APK 或 AAB 文件,自动解析 AndroidManifest.xml 提取元数据 +- **项目模式**:选择 `.csproj` 项目文件,自动执行 `dotnet publish` 构建,然后定位产物并解析 + +### 输入 + +#### 文件模式 + +| 字段 | 必填 | 说明 | +|------|------|------| +| APK/AAB 文件 | ✅ | 选择 `.apk` 或 `.aab` 文件,工具自动识别格式类型 | +| Output Directory | ❌ | 版本记录 JSON 输出目录,为空时输出到桌面 | +| ProductId | ✅ | 产品标识 GUID,用于区分服务端产品线 | +| Platform | ✅ | 目标平台(默认 Android = 4) | +| Product Name | ❌ | 产品名称,写入版本记录 | +| Release Notes | ❌ | 发布说明 | +| Is Forcibly | ❌ | 是否强制更新 | + +#### 项目模式 + +| 字段 | 必填 | 说明 | +|------|------|------| +| `.csproj` 文件 | ✅ | 选择 MAUI 或 Avalonia Android 项目的 `.csproj`,工具自动解析 `ApplicationId`、`ApplicationDisplayVersion`、`ApplicationVersion` | +| Output Directory | ❌ | 同上 | + +### 操作流程 + +1. **选择模式**:切换 File Mode / Project Mode 开关。 + - **文件模式**:点击 **Select File** 选择 `.apk` 或 `.aab` 文件。 + - **项目模式**:点击 **Select Project** 选择 `.csproj` 文件,然后点击 **Build & Locate** 自动执行 `dotnet publish` 并定位产物。 +2. **Analyze**:点击 Analyze 按钮,工具自动: + - 识别文件格式(APK / Android App Bundle) + - 从 AndroidManifest.xml 中提取 `PackageName`、`VersionName`、`VersionCode` + - 计算 SHA256 哈希和文件大小 +3. **填写上传配置**:确认或编辑自动提取的元数据,填写 ProductId、Product Name 等发布信息。 +4. **Upload**:点击 Upload 上传到服务端,并自动生成版本记录 JSON 文件。 + +### 工具内部做了什么 + +1. **格式检测**:通过文件扩展名(`.apk` / `.aab`)和 ZIP 内部结构(`AndroidManifest.xml` 位置)自动识别包格式。 +2. **元数据解析**:使用 `AxmlParser` 从 ZIP 中读取二进制的 `AndroidManifest.xml`,提取 `package`、`versionName`、`versionCode`。 +3. **SHA256 计算**:对完整文件计算 SHA256 哈希值。 +4. **文件大小**:读取文件长度并格式化为人类可读的显示(KB/MB/GB)。 +5. **项目构建**(仅项目模式):调用 `dotnet publish -c Release -o {publishDir}`,自动定位输出的 APK/AAB 文件。 +6. **上传**:通过 HTTP multipart/form-data 将文件和表单字段(Name、Version、Hash、Format、Size、Platform、ProductId、IsForcibly)上传到服务端。 +7. **版本记录导出**:上传成功后生成 `mobile_version_{timestamp}.json`,包含完整的版本元数据。 + +### 输出 + +``` +{OutputDirectory}/mobile_version_20260614120000.json +``` + +```json +{ + "name": "MyApp", + "version": "2.0.0", + "hash": "a1b2c3d4e5f6...", + "url": "https://server.example.com/packages/app-v2.0.0.apk", + "packageName": "com.example.myapp", + "fileSize": 50000000, + "format": "apk", + "platform": 4, + "productId": "2d974e2a-31e6-4887-9bb1-b4689e98c77a", + "isForcibly": false, + "releaseDate": "2026-06-14T12:00:00.0000000Z" +} +``` + +### 下游如何使用 + +- 将生成的版本记录 JSON 导入或上传到你的更新服务端(如 GeneralSpacestation) +- 客户端(`GeneralUpdate.Avalonia.Android` 或 `GeneralUpdate.Maui.Android`)从服务端查询版本信息时,返回的数据结构与版本记录的内容对应 +- 客户端下载 APK 后使用 `hash` 字段做 SHA256 完整性校验 + +### 支持的 AndroidManifest 字段提取 + +| 清单属性 | 字段名 | 说明 | +|---------|--------|------| +| `package` | `PackageName` | 应用包名(唯一标识) | +| `android:versionName` | `VersionName` | 展示版本号(如 `2.0.0`) | +| `android:versionCode` | `VersionCode` | 内部版本代码(整数) | + +--- + ## 推荐发布工作流 -这个顺序把六个模块串联成一个完整的发布流水线: +### 桌面端更新 + +这个顺序把 Patch、OSS、Config、Simulation 四个模块串联成一个完整的桌面应用发布流水线: ``` ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ @@ -343,6 +438,21 @@ Generate Sample 额外输出: 5. **Simulation**:选择旧版本目录和补丁 ZIP,确认 PASS。 6. **发布上线**:上传补丁 ZIP、OSS 清单到生产环境。 +### 移动端更新 + +使用 Mobile 模块串联移动端 Android 应用发布: + +``` +┌────────────┐ ┌──────────┐ ┌──────────┐ +│ 构建 APK/ │ -> │ Mobile │ -> │ 发布上线 │ +│ AAB 产物 │ │ 解析+上传 │ │ 更新服务 │ +└────────────┘ └──────────┘ └──────────┘ +``` + +1. **构建产物**:在 CI 或本地构建出 `.apk` / `.aab` 文件。 +2. **Mobile**:选择文件或项目,自动解析元数据、计算 SHA256、上传服务端、导出版本记录。 +3. **发布上线**:确认上传成功,客户端通过 `GeneralUpdate.Avalonia.Android` 或 `GeneralUpdate.Maui.Android` 检查更新。 + --- ## 常见问题 @@ -364,4 +474,6 @@ Generate Sample 额外输出: - [GeneralUpdate.Core](../doc/GeneralUpdate.Core):Client/Upgrade 更新主流程 - [GeneralUpdate.Differential](../doc/GeneralUpdate.Differential):差分算法 Clean/Dirty 模式 - [GeneralUpdate.Extension](../doc/GeneralUpdate.Extension):扩展包安装与版本管理 +- [GeneralUpdate.Avalonia.Android](../doc/GeneralUpdate.Avalonia.Android):Avalonia Android APK 更新组件 +- [GeneralUpdate.Maui.Android](../doc/GeneralUpdate.Maui.Android):MAUI Android APK 更新组件 - [入门实战手册](./Beginner%20cookbook):从零跑通完整更新闭环 diff --git a/website/docs/quickstart/Maui Android cookbook.md b/website/docs/quickstart/Maui Android cookbook.md new file mode 100644 index 0000000..e2d53bd --- /dev/null +++ b/website/docs/quickstart/Maui Android cookbook.md @@ -0,0 +1,240 @@ +--- +sidebar_position: 5 +title: MAUI Android 更新入门 +--- + +# GeneralUpdate.Maui.Android 快速入门 + +这篇手册面向需要在 MAUI 应用中集成 Android APK 自动更新的开发者。目标是用最少的代码跑通"检查更新 → 下载 APK → 校验 SHA256 → 启动安装器"的完整流程。 + +:::info 前置知识 +这篇手册假设你已经有一个 .NET MAUI 项目并配置好了 Android 目标框架。如果你还没有 MAUI Android 项目,请先参考 [.NET MAUI 官方文档](https://learn.microsoft.com/dotnet/maui/) 创建。 +::: + +## 更新流程 + +``` +① 检查版本 ② 下载 APK ③ 安装 +┌──────────┐ ┌──────────┐ ┌──────────────┐ +│ Client │──POST──→ │ Server │ │ Android │ +│ (MAUI) │←─JSON─── │(更新服务)│ │ Package │ +└────┬─────┘ └────┬─────┘ │ Installer │ + │ │ └──────┬───────┘ + │ 服务器返回包信息 │ │ + │ ←────────────────────│ │ + │ │ │ + │ GET /packages/app-v2.0.0.apk │ + │ ────────────────────→│ │ + │ APK 文件 (支持 Range)│ │ + │ ←────────────────────│ │ + │ │ │ + │ SHA256 校验通过后 │ │ + │ 启动 Package Installer ─────────────────→ │ + │ │ │ +``` + +| 角色 | 定义 | 负责什么 | +| --- | --- | --- | +| Client(MAUI) | 你的 MAUI Android 应用 | 检查版本 → 下载 APK → 校验 → 启动安装器 | +| Server | 更新服务(如 Samples 中的 Server) | 返回版本信息、提供 APK 下载 | +| Android Package Installer | Android 系统组件 | 安装 APK(验证签名、替换应用) | + +## Phase 1:环境准备 + +### 安装清单 + +| 项 | 要求 | 验证命令 | +| --- | --- | --- | +| .NET SDK | 10.0+ | `dotnet --version` | +| MAUI 工作负载 | 已安装 | `dotnet workload list`(应包含 `maui`) | + +### NuGet 包 + +```bash +dotnet add package GeneralUpdate.Maui.Android +``` + +## Phase 2:FileProvider 配置 + +APK 文件需要通过 FileProvider 传递给 Android Package Installer。在 `Platforms/Android/AndroidManifest.xml` 中添加: + +```xml + + + + + +``` + +创建 `Platforms/Android/Resources/xml/file_paths.xml`: + +```xml + + + + +``` + +## Phase 3:编写更新代码 + +### 方式一:DI 容器集成(推荐) + +在 `MauiProgram.cs` 中注册服务: + +```csharp +using GeneralUpdate.Maui.Android.Services; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => { }); + + // 注册更新服务 + builder.Services.AddGeneralUpdateMauiAndroid(); + + // 注册应用服务 + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + return builder.Build(); + } +} +``` + +在 ViewModel 中使用: + +```csharp +using GeneralUpdate.Maui.Android.Abstractions; +using GeneralUpdate.Maui.Android.Models; + +public class MainViewModel +{ + private readonly IAndroidBootstrap _bootstrap; + + public MainViewModel(IAndroidBootstrap bootstrap) + { + _bootstrap = bootstrap; + + _bootstrap.AddListenerDownloadProgressChanged += (_, args) => + { + MainThread.BeginInvokeOnMainThread(() => + { + OnProgressChanged?.Invoke(this, args.Statistics.ProgressPercentage); + }); + }; + } + + public async Task CheckAndUpdateAsync() + { + var package = new UpdatePackageInfo + { + Version = "2.0.0", + DownloadUrl = "http://10.0.2.2:5000/packages/app-v2.0.0.apk", + Sha256 = "服务端返回的SHA256哈希值", + PackageSize = 50_000_000, + ApkFileName = "app-v2.0.0.apk" + }; + + var options = new UpdateOptions + { + CurrentVersion = "1.0.0", + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = "com.mycompany.myapp.fileprovider" + } + }; + + // 检查更新 + var checkResult = await _bootstrap.ValidateAsync(package, options, CancellationToken.None); + if (!checkResult.IsUpdateAvailable) + { + await Shell.Current.DisplayAlert("提示", "已是最新版本。", "确定"); + return; + } + + // 执行更新(下载 → 校验 → 安装) + var result = await _bootstrap.ExecuteUpdateAsync(package, options, CancellationToken.None); + + if (result.IsSuccess) + await Shell.Current.DisplayAlert("更新", "安装器已启动,请在设备上确认安装。", "确定"); + else + await Shell.Current.DisplayAlert("更新失败", result.Message, "确定"); + } + + public event EventHandler? OnProgressChanged; +} +``` + +### 方式二:直接创建(无 DI) + +```csharp +using GeneralUpdate.Maui.Android; +using GeneralUpdate.Maui.Android.Models; + +var bootstrap = GeneralUpdateBootstrap.CreateDefault(); + +var package = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "http://10.0.2.2:5000/packages/app-v2.0.0.apk", + Sha256 = "服务端返回的SHA256哈希值" +}; + +var options = new UpdateOptions +{ + CurrentVersion = "1.0.0", + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = "com.mycompany.myapp.fileprovider" + } +}; + +// 二合一 API +var result = await bootstrap.ExecuteUpdateAsync(package, options, CancellationToken.None); +Console.WriteLine(result.IsSuccess ? "更新完成" : $"更新失败: {result.Message}"); +``` + +## Phase 4:启动 Server + +使用 Samples 仓库中的 Server: + +```bash +git clone https://github.com/GeneralLibrary/GeneralUpdate-Samples.git +cd GeneralUpdate-Samples/src/Server +dotnet run +``` + +Server 默认在 `http://localhost:5000` 启动。Android 模拟器中请使用 `http://10.0.2.2:5000` 访问宿主机的 localhost。 + +## Phase 5:端到端验证 + +1. 将 APK 包放到 `Server/wwwroot/packages/` 目录 +2. 更新 `wwwroot/packages/versions.json` 添加版本信息 +3. 在模拟器或真机上运行 MAUI Android 应用 +4. 观察应用输出:检查更新 → 下载 → 校验 → 启动安装器 + +### 预期输出 + +``` +Checking for updates... +Update v2.0.0 found. +Downloading... 45% 1.2MB/s +Downloading... 100% +SHA256 verified. +Installer launched — complete install on device. +``` + +## 下一步 + +- [GeneralUpdate.Maui.Android 组件文档](../doc/GeneralUpdate.Maui.Android):完整 API 参考、配置说明和高级用法 +- [MAUI Android 更新示例](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/UI/MauiUpdate):完整的 MAUI Android 更新示例项目 diff --git a/website/docs/releaselog/GeneralUpdateReleaselog.md b/website/docs/releaselog/GeneralUpdateReleaselog.md index 18409e2..e05d380 100644 --- a/website/docs/releaselog/GeneralUpdateReleaselog.md +++ b/website/docs/releaselog/GeneralUpdateReleaselog.md @@ -7,6 +7,18 @@ tags: [log] # 📒Release log +### 📍2026-06-14 + +- **GeneralUpdate.Core**: 修复代码审计发现的 BSDIFF 溢出、Zip 路径遍历、ProcessExit 死锁、IPC 密钥硬编码等严重/高危安全问题;修复下载管道失败后继续启动 Upgrade 的错误行为;备份功能默认关闭(不再默认备份);移除 UpgradeMode 透传字段;新增跨版本优先更新(CVP-first)策略,自动降级为链式更新 +- **GeneralUpdate.Differential**: BSDIFF 溢出长度校验修复 +- **GeneralUpdate.Bowl**: 修复 Trace Listener 生命周期问题 +- **GeneralUpdate.Drivelution**: 修复策略生命周期问题 +- **GeneralUpdate.Extension**: 修复 HubConnection 生命周期和 RetryPolicy 问题 +- **GeneralUpdate.Avalonia.Android**: **首个公开发布** `v0.0.1-beta.9` — Avalonia Android APK 自动更新组件,提供版本对比、断点续传下载、SHA256 校验和 Package Installer 触发编排;支持 HMAC/Bearer/ApiKey/Basic 多协议认证 +- **GeneralUpdate.Maui.Android**: **首个公开发布** `v0.0.1-beta.3` — MAUI Android APK 自动更新组件,提供 `ValidateAsync` + `ExecuteUpdateAsync` 双阶段 API、DI 容器集成、HTTP Range 断点续传和 SHA256 校验 +- **GeneralUpdate.Tools**: 新增 Mobile 打包模块,支持 APK/AAB 文件解析(自动提取 AndroidManifest 元数据)、`dotnet publish` 项目构建、SHA256 计算、服务端上传和版本记录导出 +- **文档**: 新增 GeneralUpdate.Avalonia.Android 和 GeneralUpdate.Maui.Android 完整组件参考文档(中英文);新增 Avalonia/Maui Android 快速入门教程;Tools 文档补充 Mobile 模块和双发布流水线说明 + ### 📍2026-05-20 — v10.5.0-beta.2 - **GeneralUpdate.Core**: 统一 `GeneralUpdateBootstrap` 入口,合并原 ClientCore 和 Core 能力;新增 `SetSource()` 轻配置入口与 `generalupdate.manifest.json` 极简接入;重构下载子系统为可替换的 5 层模型(Source/Policy/Executor/Pipeline/Orchestrator);扩展点体系增至 10 个可替换接口 diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/Component Introduction.md b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/Component Introduction.md index 84da1c9..068910c 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/Component Introduction.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/Component Introduction.md @@ -93,7 +93,8 @@ Separate Client (your main app), Upgrade (upgrade process), Server (update servi | Name | Description | Address | | --------------------- | -------------------------- | ------------------------------------------------------------ | | GeneralUpdate | Automatic Updates | [GitHub](https://github.com/GeneralLibrary/GeneralUpdate)
[Gitee](https://gitee.com/GeneralLibrary/GeneralUpdate)
[GitCode](https://gitcode.com/GeneralLibrary/GeneralUpdate) | -| GeneralUpdate.Maui | Maui Updates (Android) | [GitHub](https://github.com/GeneralLibrary/GeneralUpdate.Maui)
[Gitee](https://gitee.com/GeneralLibrary/GeneralUpdate.Maui)
[GitCode](https://gitcode.com/GeneralLibrary/GeneralUpdate-Maui) | +| GeneralUpdate.Maui | MAUI Android Auto-Updates | [GitHub](https://github.com/GeneralLibrary/GeneralUpdate.Maui)
[Gitee](https://gitee.com/GeneralLibrary/GeneralUpdate.Maui)
[GitCode](https://gitcode.com/GeneralLibrary/GeneralUpdate-Maui) | +| GeneralUpdate.Avalonia | Avalonia Android Auto-Updates | [GitHub](https://github.com/GeneralLibrary/GeneralUpdate.Avalonia) | | GeneralUpdate.Tools | Update Patch Creation Tool | [GitHub](https://github.com/GeneralLibrary/GeneralUpdate.Tools)
[Gitee](https://gitee.com/GeneralLibrary/GeneralUpdate.Tools)
[GitCode](https://gitcode.com/GeneralLibrary/GeneralUpdate-Tools) | | GeneralUpdate-Samples | Usage Examples | [GitHub](https://github.com/GeneralLibrary/GeneralUpdate-Samples)
[Gitee](https://gitee.com/GeneralLibrary/GeneralUpdate-Samples)
[GitCode](https://gitcode.com/GeneralLibrary/GeneralUpdate-Samples) | diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Avalonia.Android.md b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Avalonia.Android.md new file mode 100644 index 0000000..757e774 --- /dev/null +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Avalonia.Android.md @@ -0,0 +1,561 @@ +--- +sidebar_position: 9 +--- + +# GeneralUpdate.Avalonia.Android + +**Namespace:** `GeneralUpdate.Avalonia.Android` | **Main Entry:** `GeneralUpdateBootstrap.CreateDefault()` | **NuGet Package:** `GeneralUpdate.Avalonia.Android` + +## 1. Component Overview + +### 1.1 Overview + +**GeneralUpdate.Avalonia.Android** is an Android auto-update component designed for Avalonia applications. It provides version comparison, resumable APK download with HTTP Range support, SHA256 integrity verification, and Android Package Installer launch orchestration. + +Unlike desktop applications where files can be directly replaced, Android APK installation must go through the system Package Installer. This component encapsulates these platform differences, allowing Avalonia developers to implement updates simply by providing package metadata. + +**Core Capabilities:** + +| Capability | Description | +| --- | --- | +| Version Comparison | Built-in `SystemVersionComparer` with custom strategy support via `IVersionComparer` | +| Resumable Download | `HttpResumableApkDownloader` with HTTP Range headers and progress reporting | +| SHA256 Verification | Automatic file hash verification against the server-provided hash on download completion | +| APK Installation | `AndroidApkInstaller` launches system Package Installer via FileProvider URI | +| HTTP Transport Config | `HttpDownloadOptions` supports SSL/TLS policies, proxy, timeout, retry, and authentication | +| Multi-Protocol Auth | HMAC-SHA256, Bearer Token, API Key, and HTTP Basic authentication | +| Event Notifications | Version discovery, download progress, completion, and failure events | +| State Snapshot | `GetSnapshot()` for current update state, failure reason, and message | +| UI Thread Dispatching | `IUpdateEventDispatcher` for dispatching events to UI thread | +| Progressive Download | Temporary `.part` files and Sidecar `.json` metadata for process-level resume | + +**Solved Business Pain Points:** +- Avalonia apps on Android need reliable in-app updates, but APK installation must go through the system installer +- Large APK downloads can be interrupted — resume support reduces re-download traffic +- Downloaded APKs need integrity checks to prevent corruption or tampering +- Flexible authentication schemes are needed for various server requirements + +:::info Android Update ≠ Desktop File Replacement +On Android, APKs must be installed via the Android Package Installer, which handles signature verification, permission granting, and app replacement. This component encapsulates these platform differences, but **APK signing and version incrementation must still be handled in your build pipeline**. +::: + +**Use Cases:** +- Avalonia cross-platform app APK auto-update on Android +- Enterprise in-house Android app distribution +- CDN / OSS distributed APK packages + +### 1.2 Environment & Dependencies + +| Item | Description | +| --- | --- | +| **NuGet Package** | `GeneralUpdate.Avalonia.Android` | +| **Target Framework** | `net10.0-android` (min Android API 26+) | +| **Dependencies** | `Xamarin.AndroidX.Core` (FileProvider support) | +| **Platform Support** | Android 8.0 (API 26) and above | + +--- + +## 2. Feature List + +| Feature | Description | Type | Required | Notes | +| --- | --- | --- | --- | --- | +| Version Check | Compare current vs target version | Core | Yes | `ValidateAsync`, supports custom comparers | +| APK Resume Download | Resumable download with HTTP Range | Core | Yes | `DownloadAndVerifyAsync` | +| SHA256 Verification | Auto-verification on completion | Core | Auto | Via `IHashValidator` | +| APK Install Trigger | Launch system Package Installer | Core | Yes | `LaunchInstallerAsync` | +| HTTP Transport Config | SSL, proxy, timeout, retry, auth | Extension | Optional | `HttpDownloadOptions` | +| Multi-Protocol Auth | HMAC/Bearer/ApiKey/Basic | Extension | Optional | Global or per-package | +| Progress Notification | Speed, bytes, percentage | Core | Optional | `AddListenerDownloadProgressChanged` | +| Completion Notification | Version found, download done, install done | Core | Optional | `AddListenerUpdateCompleted` | +| Failure Notification | Failure reason, exception, package info | Core | Optional | `AddListenerUpdateFailed` | +| Validation Notification | Update available event | Core | Optional | `AddListenerValidate` | +| State Snapshot | Get current update state anytime | Extension | Optional | `GetSnapshot()` | +| UI Thread Dispatch | Custom event dispatch strategy | Extension | Optional | Implement `IUpdateEventDispatcher` | +| Custom Downloader | Custom download implementation | Extension | Optional | Implement `IUpdateDownloader` | +| Custom Hash Validator | Custom hash algorithm | Extension | Optional | Implement `IHashValidator` | +| Custom Version Comparer | Custom version comparison | Extension | Optional | Implement `IVersionComparer` | +| Custom APK Installer | Custom install implementation | Extension | Optional | Implement `IApkInstaller` | +| Custom File Storage | Custom file I/O | Extension | Optional | Implement `IFileStorage` | +| Custom SSL Policy | Custom certificate validation | Extension | Optional | Implement `ISslValidationPolicy` | +| Custom HTTP Auth | Custom request authentication | Extension | Optional | Implement `IHttpAuthProvider` | + +--- + +## 3. API Configuration + +### 3.1 Properties + +**AndroidUpdateOptions:** + +| Field | Type | Default | Required | Description | +| --- | --- | --- | --- | --- | +| `DownloadDirectoryPath` | `string` | `""` (auto: `CacheDir/update` or `TempPath/update`) | Optional | Download directory path | +| `FileProviderAuthority` | `string` | `""` | **Yes** | FileProvider authority matching AndroidManifest | +| `TemporaryFileExtension` | `string` | `".part"` | Optional | Temp file extension | +| `SidecarExtension` | `string` | `".json"` | Optional | Resume metadata file extension | +| `DownloadBufferSize` | `int` | `65536` (64 KB) | Optional | Download buffer size (bytes) | +| `SpeedSmoothingWindowSeconds` | `int` | `4` | Optional | Speed smoothing window (seconds) | + +**HttpDownloadOptions:** + +| Field | Type | Default | Required | Description | +| --- | --- | --- | --- | --- | +| `SslValidationPolicy` | `ISslValidationPolicy?` | `null` | Optional | Custom SSL validation; `null` uses system default | +| `RequestTimeout` | `TimeSpan` | `30s` | Optional | Per-request timeout | +| `DownloadTimeout` | `TimeSpan` | `10min` | Optional | Overall download timeout | +| `Proxy` | `IWebProxy?` | `null` | Optional | HTTP proxy | +| `UseProxy` | `bool` | `false` | Optional | Whether to use configured proxy | +| `MaxRetryAttempts` | `int` | `3` | Optional | Max retry count (3 = 1 initial + 2 retries) | +| `RetryBaseDelay` | `TimeSpan` | `1s` | Optional | Exponential backoff base: `baseDelay * 2^attempt` | +| `AuthProvider` | `IHttpAuthProvider?` | `null` | Optional | Global auth provider; per-package takes precedence | + +**UpdatePackageInfo:** + +| Field | Type | Default | Required | Description | +| --- | --- | --- | --- | --- | +| `Version` | `string` | — | **Yes** | Target version | +| `DownloadUrl` | `string` | — | **Yes** | APK download URL | +| `Sha256` | `string` | — | **Yes** | SHA256 hash for integrity check | +| `FileSize` | `long` | `0` | Optional | File size in bytes | +| `FileName` | `string?` | `null` | Optional | Downloaded file name | +| `IsForced` | `bool` | `false` | Optional | Force update | +| `VersionName` | `string?` | `null` | Optional | Display version name | +| `Description` | `string?` | `null` | Optional | Version description | +| `PublishTime` | `DateTimeOffset?` | `null` | Optional | Publish time | +| `AuthScheme` | `AuthScheme?` | `null` | Optional | Per-package auth scheme | +| `AuthToken` | `string?` | `null` | Optional | Bearer or API Key token | +| `AuthSecretKey` | `string?` | `null` | Optional | HMAC-SHA256 secret key | +| `BasicUsername` | `string?` | `null` | Optional | HTTP Basic username | +| `BasicPassword` | `string?` | `null` | Optional | HTTP Basic password | + +**UpdateState Enum:** + +| Value | Ordinal | Description | +| --- | --- | --- | +| `None` | 0 | Initial state | +| `Checking` | 1 | Checking for updates | +| `UpdateAvailable` | 2 | Update found | +| `Downloading` | 3 | Downloading | +| `Verifying` | 4 | Verifying integrity | +| `ReadyToInstall` | 5 | Ready to install | +| `Installing` | 6 | Installing | +| `Completed` | 7 | Update completed | +| `Failed` | 8 | Update failed | +| `Canceled` | 9 | Canceled | + +**AuthScheme Enum:** + +| Value | Description | +| --- | --- | +| `Hmac` | HMAC-SHA256 signature authentication | +| `Bearer` | Bearer Token authentication | +| `ApiKey` | API Key authentication | +| `Basic` | HTTP Basic authentication | + +### 3.2 Instance Methods + +**GeneralUpdateBootstrap (Static Factory):** + +| Method | Parameters | Use Case | Notes | +| --- | --- | --- | --- | +| `CreateDefault(AndroidUpdateOptions, ...)` | `options` — update options; optional: `contextProvider`, `activityProvider`, `httpClient`, `versionComparer`, `eventDispatcher`, `logger`, `httpOptions` | Create default Android bootstrap instance | Null params use built-in defaults | + +**IAndroidBootstrap:** + +| Method | Parameters | Use Case | Notes | +| --- | --- | --- | --- | +| `ValidateAsync(UpdatePackageInfo, string, CancellationToken)` | `packageInfo`, `currentVersion`, `ct` | Check for updates | Returns `UpdateCheckResult` with `UpdateFound` and `TargetVersion` | +| `DownloadAndVerifyAsync(UpdatePackageInfo, CancellationToken)` | `packageInfo`, `ct` | Download APK and verify SHA256 | `FilePath` contains APK path on success | +| `LaunchInstallerAsync(UpdatePackageInfo, string, CancellationToken)` | `packageInfo`, `apkFilePath`, `ct` | Launch Android Package Installer | Requires correct FileProvider authority | +| `GetSnapshot()` | None | Get current state snapshot | Returns `UpdateStateSnapshot(State, FailureReason, Message)` | +| `Dispose()` | None | Release downloader resources | — | + +### 3.3 Events + +| Event | Args | Triggered | Usage | +| --- | --- | --- | --- | +| `AddListenerValidate` | `ValidateEventArgs` — `PackageInfo`, `CurrentVersion` | When update is found after version comparison | UI display of new version info | +| `AddListenerDownloadProgressChanged` | `DownloadProgressChangedEventArgs` — `ProgressPercentage`, `DownloadSpeedBytesPerSecond`, `DownloadedBytes`, `RemainingBytes`, `TotalBytes`, `PackageInfo`, `StatusDescription` | On download progress update | Progress bar display | +| `AddListenerUpdateCompleted` | `UpdateCompletedEventArgs` — `Result` (`UpdateOperationResult`) | On download or install completion | UI state transition | +| `AddListenerUpdateFailed` | `UpdateFailedEventArgs` — `Result` (`UpdateOperationResult`) | On any stage failure | Error display with reason and exception | + +--- + +## 4. Advanced Examples + +### 4.1 Extensibility Overview + +All services can be replaced via `CreateDefault` optional parameters: + +| Interface | Default Implementation | Description | +| --- | --- | --- | +| `IVersionComparer` | `SystemVersionComparer` | Version comparison strategy | +| `IUpdateDownloader` | `HttpResumableApkDownloader` | APK download | +| `IHashValidator` | `Sha256HashValidator` | SHA256 hash verification | +| `IApkInstaller` | `AndroidApkInstaller` | APK install trigger | +| `IFileStorage` | `PhysicalFileStorage` | File storage operations | +| `IUpdateEventDispatcher` | `ImmediateEventDispatcher` | Event dispatcher (direct invoke) | +| `IUpdateLogger` | `NoOpUpdateLogger` | Logger | +| `IAndroidContextProvider` | `DefaultAndroidContextProvider` | Android Context provider | +| `IAndroidActivityProvider` | `NullAndroidActivityProvider` | Android Activity provider | +| `ISslValidationPolicy` | Via `HttpDownloadOptions` | SSL validation policy | +| `IHttpAuthProvider` | Via `HttpDownloadOptions` or per-package | HTTP auth provider | + +### 4.2 Scenario Examples + +#### Scenario 1: Custom Version Comparer + +```csharp +using GeneralUpdate.Avalonia.Android.Abstractions; + +public sealed class CustomDateVersionComparer : IVersionComparer +{ + public bool TryCompare(string currentVersion, string targetVersion, + out int compareResult, out string? errorMessage) + { + try + { + var current = ParseVersion(currentVersion); + var target = ParseVersion(targetVersion); + compareResult = target.CompareTo(current); + errorMessage = null; + return true; + } + catch (Exception ex) + { + compareResult = 0; + errorMessage = ex.Message; + return false; + } + } + + private static DateTime ParseVersion(string version) => + DateTime.ParseExact(version, "yyyy.MM.dd.fff", null); +} + +// Usage +var bootstrap = GeneralUpdateBootstrap.CreateDefault( + options, + versionComparer: new CustomDateVersionComparer()); +``` + +#### Scenario 2: Custom Event Dispatcher (Avalonia UI Thread) + +```csharp +using GeneralUpdate.Avalonia.Android.Abstractions; + +public sealed class AvaloniaEventDispatcher : IUpdateEventDispatcher +{ + public void Dispatch(Action callback) + { + if (Avalonia.Threading.Dispatcher.UIThread.CheckAccess()) + callback(); + else + Avalonia.Threading.Dispatcher.UIThread.Post(callback); + } +} + +var bootstrap = GeneralUpdateBootstrap.CreateDefault( + options, + eventDispatcher: new AvaloniaEventDispatcher()); +``` + +#### Scenario 3: Custom Downloader + +```csharp +using GeneralUpdate.Avalonia.Android.Abstractions; +using GeneralUpdate.Avalonia.Android.Models; + +public sealed class EnterpriseDownloader : IUpdateDownloader +{ + private readonly HttpClient _httpClient; + private readonly string _enterpriseToken; + + public EnterpriseDownloader(HttpClient httpClient, string enterpriseToken) + { + _httpClient = httpClient; + _enterpriseToken = enterpriseToken; + } + + public async Task DownloadAsync( + UpdatePackageInfo packageInfo, + Action? progressCallback, + CancellationToken cancellationToken = default) + { + var request = new HttpRequestMessage(HttpMethod.Get, packageInfo.DownloadUrl); + request.Headers.Add("X-Enterprise-Token", _enterpriseToken); + // Custom download and progress reporting... + } +} +``` + +--- + +## 5. Basic Usage Examples + +### 5.1 Quick Start (Minimal Demo) + +```csharp +using GeneralUpdate.Avalonia.Android; +using GeneralUpdate.Avalonia.Android.Models; + +var options = new AndroidUpdateOptions +{ + FileProviderAuthority = "com.myapp.fileprovider" +}; + +var bootstrap = GeneralUpdateBootstrap.CreateDefault(options); + +var updatePackage = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "https://update.example.com/app-v2.0.0.apk", + Sha256 = "expected-sha256-hash", + FileSize = 50_000_000, + FileName = "app_update_2.0.0.apk" +}; + +var checkResult = await bootstrap.ValidateAsync( + updatePackage, currentVersion: "1.0.0"); + +if (!checkResult.UpdateFound) +{ + Console.WriteLine("Already up to date."); + return; +} + +var downloadResult = await bootstrap.DownloadAndVerifyAsync(updatePackage); +if (!downloadResult.Success) +{ + Console.WriteLine($"Download failed: {downloadResult.Message}"); + return; +} + +var installResult = await bootstrap.LaunchInstallerAsync( + updatePackage, downloadResult.FilePath!); + +if (installResult.Success) + Console.WriteLine("Installer launched."); +``` + +### 5.2 Parameter Combination Example + +```csharp +using GeneralUpdate.Avalonia.Android; +using GeneralUpdate.Avalonia.Android.Abstractions; +using GeneralUpdate.Avalonia.Android.Models; +using GeneralUpdate.Avalonia.Android.Services; + +var options = new AndroidUpdateOptions +{ + FileProviderAuthority = "com.myapp.fileprovider", + DownloadBufferSize = 128 * 1024 +}; + +var httpOptions = new HttpDownloadOptions +{ + RequestTimeout = TimeSpan.FromSeconds(60), + DownloadTimeout = TimeSpan.FromMinutes(30), + MaxRetryAttempts = 5, + SslValidationPolicy = new AllowAllSslValidationPolicy() +}; + +var bootstrap = GeneralUpdateBootstrap.CreateDefault( + options, httpOptions: httpOptions); + +bootstrap.AddListenerDownloadProgressChanged += (_, e) => +{ + Console.WriteLine($"Download: {e.ProgressPercentage:F1}% " + + $"({e.DownloadedBytes}/{e.TotalBytes})"); +}; + +var package = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "https://cdn.example.com/app-v2.0.0.apk", + Sha256 = "expected-sha256-hash", + FileSize = 50_000_000, + FileName = "app-v2.0.0.apk" +}; + +var checkResult = await bootstrap.ValidateAsync(package, "1.0.0"); +if (!checkResult.UpdateFound) return; + +var downloadResult = await bootstrap.DownloadAndVerifyAsync(package); +if (!downloadResult.Success) return; + +await bootstrap.LaunchInstallerAsync(package, downloadResult.FilePath!); +``` + +### 5.3 Production Example + +```csharp +using GeneralUpdate.Avalonia.Android; +using GeneralUpdate.Avalonia.Android.Enums; +using GeneralUpdate.Avalonia.Android.Models; + +public sealed class AppUpdateService +{ + private IAndroidBootstrap? _bootstrap; + private bool _disposed; + + public event EventHandler? ProgressChanged; + public event EventHandler? StatusChanged; + + public void Initialize(string fileProviderAuthority, string serverToken) + { + var httpOptions = new HttpDownloadOptions + { + MaxRetryAttempts = 3, + AuthProvider = new Services.BearerTokenAuthProvider(serverToken) + }; + + _bootstrap = GeneralUpdateBootstrap.CreateDefault( + new AndroidUpdateOptions { FileProviderAuthority = fileProviderAuthority }, + httpOptions: httpOptions); + + WireEvents(); + } + + public async Task UpdateAsync( + UpdatePackageInfo package, string currentVersion, + CancellationToken ct = default) + { + if (_bootstrap == null) + throw new InvalidOperationException("Not initialized."); + + StatusChanged?.Invoke(this, "Checking for updates..."); + var checkResult = await _bootstrap.ValidateAsync(package, currentVersion, ct); + if (!checkResult.UpdateFound) + { + StatusChanged?.Invoke(this, "Already up to date."); + return true; + } + + StatusChanged?.Invoke(this, "Downloading..."); + var downloadResult = await _bootstrap.DownloadAndVerifyAsync(package, ct); + if (!downloadResult.Success) + { + StatusChanged?.Invoke(this, $"Download failed: {downloadResult.Message}"); + return false; + } + + if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O) + { + var ctx = Android.App.Application.Context; + var pm = ctx.PackageManager; + if (pm != null && !pm.CanRequestPackageInstalls()) + { + StatusChanged?.Invoke(this, "Please allow install from unknown apps."); + var intent = new Android.Content.Intent( + Android.Provider.Settings.ActionManageUnknownAppSources) + .SetData(Android.Net.Uri.Parse("package:" + ctx.PackageName)); + intent.AddFlags(Android.Content.ActivityFlags.NewTask); + ctx.StartActivity(intent); + return false; + } + } + + StatusChanged?.Invoke(this, "Installing..."); + var installResult = await _bootstrap.LaunchInstallerAsync( + package, downloadResult.FilePath!, ct); + + if (installResult.Success) + { + StatusChanged?.Invoke(this, "Installer launched."); + return true; + } + + StatusChanged?.Invoke(this, $"Install failed: {installResult.Message}"); + return false; + } + + private void WireEvents() + { + if (_bootstrap == null) return; + _bootstrap.AddListenerDownloadProgressChanged += (_, e) => + ProgressChanged?.Invoke(this, e.ProgressPercentage); + _bootstrap.AddListenerUpdateFailed += (_, e) => + StatusChanged?.Invoke(this, $"Failed: {e.Result.Message}"); + } + + public void Dispose() + { + if (_disposed) return; + _bootstrap?.Dispose(); + _disposed = true; + } +} +``` + +--- + +## 6. Global Configuration + +### Authentication + +| Scheme | AuthScheme | Required Fields | +| --- | --- | --- | +| HMAC-SHA256 | `Hmac` | `AuthSecretKey` | +| Bearer Token | `Bearer` | `AuthToken` | +| API Key | `ApiKey` | `AuthToken` | +| HTTP Basic | `Basic` | `BasicUsername` + `BasicPassword` | + +### SSL Policy + +| Policy | Class | Use Case | +| --- | --- | --- | +| System Default | `null` | Production | +| Allow All (dev only) | `AllowAllSslValidationPolicy` | Development with self-signed certs | +| Custom | Implement `ISslValidationPolicy` | Private CA or certificate pinning | + +### Resume Download + +Downloader uses temporary files (`.part`) and metadata files (`.json`): + +``` +CacheDir/update/ +├── app-v2.0.0.apk.part # Partial download temp file +├── app-v2.0.0.apk.json # Resume metadata +└── app-v2.0.0.apk # Final file +``` + +### AndroidManifest + +```xml + + + + + +``` + +`Resources/xml/file_paths.xml`: +```xml + + + + +``` + +### Platform Notes + +| Item | Details | +| --- | --- | +| Min API | 26 (Android 8.0) | +| Install Permission | Android 8.0+ requires `CanRequestPackageInstalls()` check | +| FileProvider | Required to pass APK URI to system installer | + +--- + +## Related Resources + +- [GeneralUpdate.Avalonia Repository](https://github.com/GeneralLibrary/GeneralUpdate.Avalonia) +- [Android Update Sample](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/UI/AndroidUpdate) +- [FileProvider Documentation](https://developer.android.com/training/secure-file-sharing/setup-sharing) diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Maui.Android.md b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Maui.Android.md new file mode 100644 index 0000000..6d6ea6a --- /dev/null +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Maui.Android.md @@ -0,0 +1,577 @@ +--- +sidebar_position: 10 +--- + +# GeneralUpdate.Maui.Android + +**Namespace:** `GeneralUpdate.Maui.Android` | **Main Entry:** `GeneralUpdateBootstrap.CreateDefault()` / `AddGeneralUpdateMauiAndroid()` | **NuGet Package:** `GeneralUpdate.Maui.Android` + +## 1. Component Overview + +### 1.1 Overview + +**GeneralUpdate.Maui.Android** is an Android auto-update component designed for .NET MAUI applications. It provides version discovery, resumable APK download, SHA256 integrity verification, and system Package Installer trigger in a unified update orchestration API. + +Unlike desktop file-replacement updates, Android APK updates must go through the system Package Installer. This component encapsulates this platform difference and provides a two-phase API: `ValidateAsync` for version checking and `ExecuteUpdateAsync` for the complete download→verify→install workflow. + +**Core Capabilities:** + +| Capability | Description | +| --- | --- | +| Version Check | Built-in `System.Version` parsing, auto-detects if target version is newer | +| Resumable APK Download | `HttpRangeDownloader` with HTTP Range headers and progress reporting | +| SHA256 Verification | Auto file hash verification, corrupted files auto-deleted on failure | +| System Installer Trigger | `AndroidApkInstaller` launches system Package Installer via FileProvider + Intent | +| DI Integration | `AddGeneralUpdateMauiAndroid()` extension registers all services in `IServiceCollection` | +| HTTP Transport Config | `HttpDownloadOptions` with SSL policies, proxy, timeout, auth, exponential backoff | +| Multi-Protocol Auth | HMAC-SHA256, Bearer Token, API Key, HTTP Basic | +| Event Notifications | Version discovery, download progress, completion stages, failure reasons | +| Concurrency Safety | `Interlocked` guard prevents concurrent `ExecuteUpdateAsync` calls | +| Temp File Management | `.downloading` temp extension, atomic rename on completion | + +**Solved Business Pain Points:** +- MAUI Android apps need built-in auto-update capability outside Google Play +- Unstable networks during large APK downloads require resume support +- Downloaded APKs need integrity verification for security +- Deep DI container integration simplifies adoption in MAUI apps + +:::info MAUI Update ≠ Desktop File Replacement +On Android, APKs must be installed via the Android Package Installer. The system handles signature verification and app replacement. This component handles the platform difference, but **APK signing and version management must still be handled in your build pipeline**. +::: + +**Use Cases:** +- MAUI Android app auto-update (enterprise distribution, beta testing, side-loading) +- CDN / OSS distributed APK packages +- DI-managed MAUI applications + +### 1.2 Environment & Dependencies + +| Item | Description | +| --- | --- | +| **NuGet Package** | `GeneralUpdate.Maui.Android` | +| **Target Framework** | `net10.0` + `net10.0-android` (multi-target; Android min API 21) | +| **Dependencies** | `Microsoft.Extensions.DependencyInjection.Abstractions`, `Microsoft.Extensions.Http`, `Xamarin.AndroidX.Core` | +| **Platform Support** | Android 5.0 (API 21) and above | + +--- + +## 2. Feature List + +| Feature | Description | Type | Required | Notes | +| --- | --- | --- | --- | --- | +| Version Check | Compare current vs target version | Core | Yes | `ValidateAsync`, based on `System.Version` | +| Update Execution | Download→Verify→Install workflow | Core | Yes | `ExecuteUpdateAsync`, concurrency safe | +| APK Resume Download | HTTP Range resume + progress | Core | Auto | `HttpRangeDownloader` | +| SHA256 Verification | Auto-verify, delete on mismatch | Core | Auto | Via `IHashValidator` | +| APK Install Trigger | FileProvider + Intent to system installer | Core | Auto | `AndroidApkInstaller`, auto permission check | +| DI Integration | One-call service registration | Extension | Recommended | `AddGeneralUpdateMauiAndroid()` | +| HTTP Transport Config | SSL, proxy, timeout, retry, auth | Extension | Optional | `HttpDownloadOptions` | +| Multi-Protocol Auth | HMAC/Bearer/ApiKey/Basic | Extension | Optional | Global or per-package | +| Progress Notification | Bytes, speed, percentage, remaining | Core | Optional | `IProgress` | +| Completion Notification | Download, verification, install, workflow | Core | Optional | `AddListenerUpdateCompleted` | +| Failure Notification | Categorized failure reason | Core | Optional | `AddListenerUpdateFailed` | +| Validation Notification | Update found event | Core | Optional | `AddListenerValidate` | +| State Query | Real-time state query | Core | Optional | `CurrentState` property | +| Concurrency Guard | Prevent duplicate execution | Core | Auto | `Interlocked` atomic operation | +| Corrupt File Auto-Cleanup | Auto-delete on verification failure | Extension | Optional | `UpdateOptions.DeleteCorruptedPackageOnFailure` | +| Progress Interval Control | Custom progress callback frequency | Extension | Optional | `UpdateOptions.ProgressReportInterval` | +| Custom Downloader | Custom download implementation | Extension | Optional | Implement `IUpdateDownloader` | +| Custom Hash Validator | Custom hash implementation | Extension | Optional | Implement `IHashValidator` | +| Custom Installer | Custom install logic | Extension | Optional | Implement `IApkInstaller` | +| Custom Storage Provider | Custom file path/storage | Extension | Optional | Implement `IUpdateStorageProvider` | +| Custom Logger | Custom logging | Extension | Optional | Implement `IUpdateLogger` | +| Custom SSL Policy | Custom certificate validation | Extension | Optional | Implement `ISslValidationPolicy` | +| Custom HTTP Auth | Custom auth provider | Extension | Optional | Implement `IHttpAuthProvider` | + +--- + +## 3. API Configuration + +### 3.1 Properties + +**UpdateOptions:** + +| Field | Type | Default | Required | Description | +| --- | --- | --- | --- | --- | +| `CurrentVersion` | `string` | `""` | **Yes** | Current app version | +| `DownloadDirectory` | `string?` | `null` | Optional | Download directory, auto-selected if null | +| `TemporaryFileExtension` | `string` | `".downloading"` | Optional | Temp file extension | +| `DeleteCorruptedPackageOnFailure` | `bool` | `true` | Optional | Auto-delete corrupted packages | +| `ProgressReportInterval` | `TimeSpan` | `500ms` | Optional | Progress report interval | +| `InstallOptions` | `AndroidInstallOptions` | `new()` | Optional | Install options | + +**AndroidInstallOptions:** + +| Field | Type | Default | Required | Description | +| --- | --- | --- | --- | --- | +| `FileProviderAuthority` | `string` | `""` | **Yes** | FileProvider authority matching AndroidManifest | +| `MimeType` | `string` | `"application/vnd.android.package-archive"` | Optional | APK MIME type | + +**HttpDownloadOptions:** + +| Field | Type | Default | Required | Description | +| --- | --- | --- | --- | --- | +| `SslValidationPolicy` | `ISslValidationPolicy?` | `null` | Optional | Custom SSL validation | +| `DownloadTimeout` | `TimeSpan` | `10min` | Optional | Overall download timeout | +| `Proxy` | `IWebProxy?` | `null` | Optional | HTTP proxy | +| `UseProxy` | `bool` | `false` | Optional | Whether to use proxy | +| `AuthProvider` | `IHttpAuthProvider?` | `null` | Optional | Global auth provider | + +**UpdatePackageInfo:** + +| Field | Type | Default | Required | Description | +| --- | --- | --- | --- | --- | +| `Version` | `string` | `""` | **Yes** | Target version | +| `DownloadUrl` | `string` | `""` | **Yes** | APK download URL | +| `Sha256` | `string` | `""` | **Yes** | SHA256 hash | +| `PackageSize` | `long?` | `null` | Optional | Package size (bytes) | +| `ApkFileName` | `string?` | `null` | Optional | APK file name | +| `ForceUpdate` | `bool` | `false` | Optional | Force update | +| `VersionName` | `string?` | `null` | Optional | Display name | +| `ReleaseNotes` | `string?` | `null` | Optional | Release notes | +| `PublishTime` | `DateTimeOffset?` | `null` | Optional | Publish time | +| `AuthScheme` | `AuthScheme?` | `null` | Optional | Auth scheme | +| `AuthToken` | `string?` | `null` | Optional | Auth token | +| `AuthSecretKey` | `string?` | `null` | Optional | HMAC secret key | +| `BasicUsername` | `string?` | `null` | Optional | Basic username | +| `BasicPassword` | `string?` | `null` | Optional | Basic password | + +**UpdateState Enum:** + +| Value | Description | +| --- | --- | +| `None` | Idle | +| `Checking` | Checking for updates | +| `UpdateAvailable` | Update found | +| `Downloading` | Downloading | +| `Verifying` | Verifying integrity | +| `ReadyToInstall` | Ready to install | +| `Installing` | Installing | +| `Completed` | Completed | +| `Failed` | Failed | +| `Canceled` | Canceled | + +**UpdateFailureReason Enum:** + +| Value | Description | +| --- | --- | +| `Unknown` | Unknown error | +| `InvalidInput` | Invalid input parameters | +| `AlreadyInProgress` | Update already in progress | +| `Network` | Network error | +| `Download` | Download error | +| `FileAccess` | File access error | +| `IntegrityCheckFailed` | SHA256 verification failed | +| `InstallPermissionDenied` | Install permission denied | +| `Installation` | Installation error | +| `Canceled` | User canceled | +| `NoUpdateAvailable` | No update available | + +**UpdateCompletionStage Enum:** + +| Value | Description | +| --- | --- | +| `DownloadCompleted` | Download complete | +| `VerificationCompleted` | Verification complete | +| `InstallationTriggered` | Installer launched | +| `WorkflowCompleted` | Workflow complete | + +### 3.2 Instance Methods + +**GeneralUpdateBootstrap (Static Factory):** + +| Method | Parameters | Use Case | Notes | +| --- | --- | --- | --- | +| `CreateDefault(HttpClient?, IUpdateLogger?, HttpDownloadOptions?)` | `httpClient` — optional external HttpClient; `logger` — optional logger; `httpOptions` — optional HTTP config | Create default bootstrap | When `httpOptions` is set, `httpClient` is ignored | +| `AddGeneralUpdateMauiAndroid(IServiceCollection, HttpClient?)` | `services` — DI container; `httpClient` — optional HttpClient | Register all update services in DI | Returns `IServiceCollection` for chaining | + +**IAndroidBootstrap:** + +| Method | Parameters | Use Case | Notes | +| --- | --- | --- | --- | +| `ValidateAsync(UpdatePackageInfo, UpdateOptions, CancellationToken)` | `packageInfo`, `options`, `ct` | Check for updates | Returns `UpdateCheckResult` with `IsUpdateAvailable` | +| `ExecuteUpdateAsync(UpdatePackageInfo, UpdateOptions, CancellationToken)` | `packageInfo`, `options`, `ct` | Execute full update workflow | Concurrency guard prevents duplicate calls | +| `Dispose()` | None | Release resources | — | +| `CurrentState` | Property (`UpdateState`) | Query current state | Thread-safe (`Volatile.Read`) | + +### 3.3 Events + +| Event | Args | Triggered | Usage | +| --- | --- | --- | --- | +| `AddListenerValidate` | `ValidateEventArgs` — `PackageInfo` | When update is found | UI display of new version info | +| `AddListenerDownloadProgressChanged` | `DownloadProgressChangedEventArgs` — `PackageInfo`, `Statistics`, `StatusDescription` | On download progress | Progress bar update | +| `AddListenerUpdateCompleted` | `UpdateCompletedEventArgs` — `PackageInfo`, `Stage`, `PackagePath` | On stage completion | Four stages: download, verification, install, workflow | +| `AddListenerUpdateFailed` | `UpdateFailedEventArgs` — `Reason`, `Message`, `Exception`, `PackageInfo` | On any failure | Structured failure reason | + +--- + +## 4. Advanced Examples + +### 4.1 Extensibility Overview + +All services can be replaced via DI after `AddGeneralUpdateMauiAndroid()`: + +| Interface | Default Implementation | Description | +| --- | --- | --- | +| `IUpdateDownloader` | `HttpRangeDownloader` | Resumable APK download | +| `IHashValidator` | `Sha256Validator` | SHA256 hash verification | +| `IApkInstaller` | `AndroidApkInstaller` | APK install trigger | +| `IUpdateStorageProvider` | `UpdateFileStore` | File path and storage operations | +| `IUpdateLogger` | `NullUpdateLogger` | Logger | +| `IUpdateBootstrap` | `AndroidBootstrap` | Update orchestration | +| `ISslValidationPolicy` | Via `HttpDownloadOptions` | SSL validation | +| `IHttpAuthProvider` | Via `HttpDownloadOptions` or per-package | HTTP auth provider | + +### 4.2 Scenario Examples + +#### Scenario 1: DI Container Integration + +```csharp +using GeneralUpdate.Maui.Android.Abstractions; +using GeneralUpdate.Maui.Android.Models; +using GeneralUpdate.Maui.Android.Services; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => { }); + + builder.Services.AddGeneralUpdateMauiAndroid(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + return builder.Build(); + } +} + +public class MainViewModel +{ + private readonly IAndroidBootstrap _bootstrap; + + public MainViewModel(IAndroidBootstrap bootstrap) + { + _bootstrap = bootstrap; + } + + public async Task CheckAndUpdateAsync() + { + var package = new UpdatePackageInfo + { + Version = "2.0.0", + DownloadUrl = "https://example.com/app-v2.apk", + Sha256 = "expected-sha256" + }; + + var options = new UpdateOptions + { + CurrentVersion = "1.0.0", + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = "com.myapp.fileprovider" + } + }; + + var result = await _bootstrap.ExecuteUpdateAsync(package, options, CancellationToken.None); + Console.WriteLine(result.IsSuccess ? "Update succeeded!" : $"Failed: {result.Message}"); + } +} +``` + +#### Scenario 2: Custom HTTP Authentication + +```csharp +using GeneralUpdate.Maui.Android.Models; +using GeneralUpdate.Maui.Android.Services; + +// Global auth provider +var httpOptions = new HttpDownloadOptions +{ + AuthProvider = new BearerTokenAuthProvider("your-token-here") +}; + +var bootstrap = GeneralUpdateBootstrap.CreateDefault( + httpClient: null, logger: null, httpOptions: httpOptions); + +// Per-package auth +var package = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "https://example.com/app-v2.apk", + Sha256 = "expected-sha256", + AuthScheme = Enums.AuthScheme.Bearer, + AuthToken = "your-token-here" +}; +``` + +#### Scenario 3: Non-DI Direct Usage + +```csharp +using GeneralUpdate.Maui.Android; +using GeneralUpdate.Maui.Android.Models; + +var bootstrap = GeneralUpdateBootstrap.CreateDefault(); + +var options = new UpdateOptions +{ + CurrentVersion = "1.0.0", + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = "com.myapp.fileprovider" + } +}; + +var package = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "https://example.com/app-v2.apk", + Sha256 = "expected-sha256" +}; + +var result = await bootstrap.ExecuteUpdateAsync(package, options, CancellationToken.None); +``` + +--- + +## 5. Basic Usage Examples + +### 5.1 Quick Start (Minimal Demo) + +```csharp +using GeneralUpdate.Maui.Android; +using GeneralUpdate.Maui.Android.Models; + +var bootstrap = GeneralUpdateBootstrap.CreateDefault(); + +var package = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "https://update.example.com/app-v2.0.0.apk", + Sha256 = "a1b2c3d4e5f6..." +}; + +var options = new UpdateOptions +{ + CurrentVersion = "1.0.0", + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = "com.myapp.fileprovider" + } +}; + +var checkResult = await bootstrap.ValidateAsync(package, options, CancellationToken.None); +if (!checkResult.IsUpdateAvailable) +{ + Console.WriteLine("Already up to date."); + return; +} + +var result = await bootstrap.ExecuteUpdateAsync(package, options, CancellationToken.None); +Console.WriteLine(result.IsSuccess ? "Update completed." : $"Update failed: {result.Message}"); +``` + +### 5.2 Parameter Combination Example + +```csharp +using GeneralUpdate.Maui.Android; +using GeneralUpdate.Maui.Android.Enums; +using GeneralUpdate.Maui.Android.Models; + +var httpOptions = new HttpDownloadOptions +{ + DownloadTimeout = TimeSpan.FromMinutes(30), + AuthProvider = new GeneralUpdate.Maui.Android.Services.BearerTokenAuthProvider("token") +}; + +var bootstrap = GeneralUpdateBootstrap.CreateDefault( + httpClient: null, logger: null, httpOptions: httpOptions); + +bootstrap.AddListenerDownloadProgressChanged += (_, e) => +{ + var s = e.Statistics; + Console.WriteLine($"Progress: {s.ProgressPercentage:F1}%"); +}; + +var options = new UpdateOptions +{ + CurrentVersion = "1.0.0", + TemporaryFileExtension = ".downloading", + DeleteCorruptedPackageOnFailure = true, + ProgressReportInterval = TimeSpan.FromMilliseconds(250), + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = "com.myapp.fileprovider" + } +}; + +var package = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "https://cdn.example.com/app-v2.0.0.apk", + Sha256 = "expected-sha256", + PackageSize = 50_000_000, + ApkFileName = "app-v2.0.0.apk" +}; + +var result = await bootstrap.ExecuteUpdateAsync(package, options, CancellationToken.None); +``` + +### 5.3 Production Example + +```csharp +using GeneralUpdate.Maui.Android; +using GeneralUpdate.Maui.Android.Abstractions; +using GeneralUpdate.Maui.Android.Enums; +using GeneralUpdate.Maui.Android.Models; +using System.Net.Http.Json; + +public sealed class MauiUpdateService : IDisposable +{ + private readonly HttpClient _httpClient; + private IAndroidBootstrap? _bootstrap; + private readonly string _fileProviderAuthority; + private bool _disposed; + + public MauiUpdateService(HttpClient httpClient, string fileProviderAuthority) + { + _httpClient = httpClient; + _fileProviderAuthority = fileProviderAuthority; + } + + public void Initialize(HttpDownloadOptions? httpOptions = null) + { + _bootstrap = GeneralUpdateBootstrap.CreateDefault( + httpClient: null, logger: null, httpOptions: httpOptions); + WireEvents(); + } + + public async Task CheckAndUpdateAsync( + string serverUrl, string currentVersion, CancellationToken ct = default) + { + if (_bootstrap == null) + throw new InvalidOperationException("Call Initialize() first."); + + try + { + var package = await FetchPackageFromServerAsync(serverUrl, currentVersion, ct); + if (package == null) return true; // No update + + var options = new UpdateOptions + { + CurrentVersion = currentVersion, + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = _fileProviderAuthority + } + }; + + var checkResult = await _bootstrap.ValidateAsync(package, options, ct); + if (!checkResult.IsUpdateAvailable) return true; + + var execResult = await _bootstrap.ExecuteUpdateAsync(package, options, ct); + return execResult.IsSuccess; + } + catch (OperationCanceledException) { return false; } + catch (HttpRequestException ex) { OnStatusChanged?.Invoke(this, $"Network: {ex.Message}"); return false; } + } + + private Task FetchPackageFromServerAsync( + string serverUrl, string currentVersion, CancellationToken ct) + { + // Implementation: query server API for package metadata + throw new NotImplementedException(); + } + + private void WireEvents() { /* wire bootstrap events */ } + + public event EventHandler? OnStatusChanged; + + public void Dispose() + { + if (_disposed) return; + _bootstrap?.Dispose(); + _disposed = true; + } +} +``` + +--- + +## 6. Global Configuration + +### DI Registration + +```csharp +// Default HttpClient +services.AddGeneralUpdateMauiAndroid(); + +// External HttpClient (shared pool) +services.AddGeneralUpdateMauiAndroid(myHttpClient); + +// IHttpClientFactory +services.AddHttpClient(client => +{ + client.Timeout = TimeSpan.FromMinutes(10); +}); +services.AddSingleton(); +``` + +### Authentication + +| Scheme | AuthScheme | Required Fields | +| --- | --- | --- | +| HMAC-SHA256 | `Hmac` | `AuthSecretKey` | +| Bearer Token | `Bearer` | `AuthToken` | +| API Key | `ApiKey` | `AuthToken` | +| HTTP Basic | `Basic` | `BasicUsername` + `BasicPassword` | + +### SSL Policy + +| Policy | Class | Use Case | +| --- | --- | --- | +| System Default | `null` | Production | +| Allow All (dev only) | `AllowAllSslValidationPolicy` | Development | +| Custom | Implement `ISslValidationPolicy` | Private CA | + +### AndroidManifest + +```xml + + + + + +``` + +`Platforms/Android/Resources/xml/file_paths.xml`: +```xml + + + + +``` + +### Platform Notes + +| Item | Details | +| --- | --- | +| Min API | 21 (Android 5.0) | +| Install Permission | Android 8.0+ requires `CanRequestPackageInstalls()` check | +| FileProvider | Required to pass APK URI to system installer | + +--- + +## Related Resources + +- [GeneralUpdate.Maui Repository](https://github.com/GeneralLibrary/GeneralUpdate.Maui) +- [MAUI Android Update Sample](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/UI/MauiUpdate) +- [FileProvider Documentation](https://developer.android.com/training/secure-file-sharing/setup-sharing) diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/_category_.json b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/_category_.json index 753b4ea..606f5c8 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/doc/_category_.json +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/doc/_category_.json @@ -3,6 +3,6 @@ "position": 2, "link": { "type": "generated-index", - "description": "GeneralUpdate component reference — covering the five non-firmware components: Core, Bowl, Differential, Drivelution, and Extension." + "description": "GeneralUpdate component reference — covering the seven non-firmware components: Core, Bowl, Differential, Drivelution, Extension, Avalonia.Android, and Maui.Android." } } diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/Avalonia Android cookbook.md b/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/Avalonia Android cookbook.md new file mode 100644 index 0000000..11497fd --- /dev/null +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/Avalonia Android cookbook.md @@ -0,0 +1,179 @@ +--- +sidebar_position: 4 +title: Avalonia Android Update Quickstart +--- + +# GeneralUpdate.Avalonia.Android Quickstart + +This guide is for developers who need to integrate Android APK auto-update into their Avalonia applications. The goal is to complete the "check update → download APK → verify SHA256 → launch installer" workflow with minimal code. + +:::info Prerequisites +This guide assumes you already have an Avalonia project configured for Android target framework. If not, please refer to the [Avalonia documentation](https://docs.avaloniaui.net/) first. +::: + +## Update Flow + +``` +① Version Check ② Download APK ③ Install +┌──────────┐ ┌──────────┐ ┌──────────────┐ +│ Client │──POST──→ │ Server │ │ Android │ +│(Avalonia)│←─JSON─── │(Update │ │ Package │ +└────┬─────┘ │ Service) │ │ Installer │ + │ └────┬─────┘ └──────┬───────┘ + │ Server returns pkg info │ + │ ←────────────────────│ │ + │ │ │ + │ GET /packages/app-v2.0.0.apk │ + │ ────────────────────→│ │ + │ APK file (Range supported) │ + │ ←────────────────────│ │ + │ │ │ + │ After SHA256 verification │ + │ launch Package Installer ────────────────→ │ +``` + +## Phase 1: Environment Setup + +### Requirements + +| Item | Requirement | Verify | +| --- | --- | --- | +| .NET SDK | 10.0+ | `dotnet --version` | +| Android SDK | API 26+ | `dotnet workload list` (should include `android`) | + +### NuGet Package + +```bash +dotnet add package GeneralUpdate.Avalonia.Android +``` + +## Phase 2: FileProvider Configuration + +Add to `AndroidManifest.xml`: + +```xml + + + + + +``` + +Create `Resources/xml/file_paths.xml`: + +```xml + + + + +``` + +## Phase 3: Write Update Code + +```csharp +using GeneralUpdate.Avalonia.Android; +using GeneralUpdate.Avalonia.Android.Models; + +public class UpdateViewModel +{ + private IAndroidBootstrap? _bootstrap; + + public async Task CheckAndUpdateAsync() + { + var options = new AndroidUpdateOptions + { + FileProviderAuthority = "com.myapp.fileprovider" + }; + + _bootstrap = GeneralUpdateBootstrap.CreateDefault(options); + + _bootstrap.AddListenerDownloadProgressChanged += (_, e) => + { + Console.WriteLine($"Download: {e.ProgressPercentage:F1}%"); + }; + + var package = new UpdatePackageInfo + { + Version = "2.0.0", + DownloadUrl = "http://10.0.2.2:5000/packages/app-v2.0.0.apk", + Sha256 = "server-provided-sha256-hash", + FileSize = 50_000_000, + FileName = "app-v2.0.0.apk" + }; + + var checkResult = await _bootstrap.ValidateAsync(package, "1.0.0"); + if (!checkResult.UpdateFound) + { + Console.WriteLine("Already up to date."); + return; + } + + var downloadResult = await _bootstrap.DownloadAndVerifyAsync(package); + if (!downloadResult.Success) + { + Console.WriteLine($"Download failed: {downloadResult.Message}"); + return; + } + + if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O) + { + var ctx = Android.App.Application.Context; + var pm = ctx.PackageManager; + if (pm != null && !pm.CanRequestPackageInstalls()) + { + var intent = new Android.Content.Intent( + Android.Provider.Settings.ActionManageUnknownAppSources) + .SetData(Android.Net.Uri.Parse("package:" + ctx.PackageName)); + intent.AddFlags(Android.Content.ActivityFlags.NewTask); + ctx.StartActivity(intent); + return; + } + } + + var installResult = await _bootstrap.LaunchInstallerAsync( + package, downloadResult.FilePath!); + + if (installResult.Success) + Console.WriteLine("Installer launched. Confirm installation on device."); + } +} +``` + +## Phase 4: Start the Server + +```bash +git clone https://github.com/GeneralLibrary/GeneralUpdate-Samples.git +cd GeneralUpdate-Samples/src/Server +dotnet run +``` + +Server runs at `http://localhost:5000`. Use `http://10.0.2.2:5000` from Android emulator to access host localhost. + +## Phase 5: End-to-End Verification + +1. Place APK in `Server/wwwroot/packages/` +2. Update `wwwroot/packages/versions.json` with version info +3. Run the Avalonia Android app on emulator/device +4. Observe: Check → Download → Verify → Install + +### Expected Output + +``` +Checking for updates... +Update v2.0.0 found. +Downloading... 45% 1.2MB/s +Downloading... 100% +SHA256 verified. +Installer launched. +``` + +## Next Steps + +- [GeneralUpdate.Avalonia.Android Component Reference](../doc/GeneralUpdate.Avalonia.Android): Full API reference and advanced usage +- [Android Update Sample](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/UI/AndroidUpdate): Complete Avalonia Android update sample project diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/GeneralUpdate.PacketTool.md b/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/GeneralUpdate.PacketTool.md index 02d7190..719e974 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/GeneralUpdate.PacketTool.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/GeneralUpdate.PacketTool.md @@ -26,9 +26,9 @@ dotnet run --project GeneralUpdate.Tools.csproj Go to [GeneralUpdate.Tools Releases](https://github.com/GeneralLibrary/GeneralUpdate.Tools/releases) and download the executable for your platform. -> The Simulation module internally runs `dotnet publish` to build test apps, so you need the .NET SDK for that feature. Patch / Extension / OSS / Config modules work without the SDK. +> The Simulation module internally runs `dotnet publish` to build test apps, so you need the .NET SDK for that feature. Patch / Extension / OSS / Config / Mobile modules work without the SDK. However, Mobile's Project Mode (Build & Locate) does require the .NET SDK. -## Six modules at a glance +## Seven modules at a glance | Module | What you provide | What the tool produces | Downstream consumer | |--------|-----------------|----------------------|---------------------| @@ -38,6 +38,7 @@ Go to [GeneralUpdate.Tools Releases](https://github.com/GeneralLibrary/GeneralUp | **Config** | Client/Upgrade `.csproj` files | `generalupdate.manifest.json` + `sample_output/` publish directory | Client/Upgrade bootstrap | | **Simulation** | Old version directory + patch ZIP | Local update server + `simulation_report.md` | Pre-release QA gate | | **Hash** | Local file (ZIP) | SHA256 lowercase hex string | Integrity check, server-side version records | +| **Mobile** | APK/AAB file or MAUI/Avalonia Android `.csproj` | `mobile_version_{timestamp}.json` version record | Mobile update server, GeneralUpdate.Avalonia/Maui components | --- @@ -305,9 +306,103 @@ In the **OSS Config** module, click **ComputeHash**, select a local ZIP file, an --- +## Mobile: Mobile packaging + +### What problem it solves + +If you use `GeneralUpdate.Avalonia.Android` or `GeneralUpdate.Maui.Android` to provide auto-update for your Android apps, each release requires extracting APK/AAB metadata (package name, version, SHA256 hash, file size) and uploading it to the server for version management. The Mobile module consolidates "parse → package → upload → generate version record" into a single interface. + +Supports two modes: + +- **File mode**: Select an APK or AAB file directly; the tool automatically parses AndroidManifest.xml to extract metadata. +- **Project mode**: Select a `.csproj`, automatically runs `dotnet publish`, then locates and parses the build artifact. + +### Inputs + +#### File mode + +| Field | Required | Description | +|-------|----------|-------------| +| APK/AAB file | ✅ | Select a `.apk` or `.aab` file; format is auto-detected | +| Output Directory | ❌ | Version record JSON output directory; defaults to desktop | +| ProductId | ✅ | Product identifier GUID for server-side product differentiation | +| Platform | ✅ | Target platform (default Android = 4) | +| Product Name | ❌ | Product name, written into the version record | +| Release Notes | ❌ | Release notes | +| Is Forcibly | ❌ | Force update flag | + +#### Project mode + +| Field | Required | Description | +|-------|----------|-------------| +| `.csproj` file | ✅ | Select a MAUI or Avalonia Android `.csproj`; tool parses `ApplicationId`, `ApplicationDisplayVersion`, `ApplicationVersion` | +| Output Directory | ❌ | Same as above | + +### Workflow + +1. **Select mode**: Toggle File Mode / Project Mode switch. + - **File mode**: Click **Select File** to choose `.apk` or `.aab`. + - **Project mode**: Click **Select Project** to choose a `.csproj`, then click **Build & Locate** to run `dotnet publish` and locate the artifact. +2. **Analyze**: Click Analyze — the tool automatically: + - Detects file format (APK / Android App Bundle) + - Extracts `PackageName`, `VersionName`, `VersionCode` from AndroidManifest.xml + - Computes SHA256 hash and file size +3. **Configure upload**: Review or edit the auto-extracted metadata, fill in ProductId, Product Name, etc. +4. **Upload**: Click Upload to send to the server, automatically generating a version record JSON file. + +### What the tool does internally + +1. **Format detection**: Identifies package format via file extension (`.apk` / `.aab`) and ZIP internal structure (AndroidManifest.xml location). +2. **Metadata parsing**: Uses `AxmlParser` to read binary `AndroidManifest.xml` from the ZIP, extracting `package`, `versionName`, `versionCode`. +3. **SHA256 computation**: Computes SHA256 hash of the full file. +4. **File size**: Reads file length and formats it for human-readable display (KB/MB/GB). +5. **Project build** (Project mode only): Runs `dotnet publish -c Release -o {publishDir}`, then auto-locates the output APK/AAB. +6. **Upload**: Sends file and form fields (Name, Version, Hash, Format, Size, Platform, ProductId, IsForcibly) via HTTP multipart/form-data. +7. **Version record export**: Generates `mobile_version_{timestamp}.json` with full version metadata after successful upload. + +### Output + +``` +{OutputDirectory}/mobile_version_20260614120000.json +``` + +```json +{ + "name": "MyApp", + "version": "2.0.0", + "hash": "a1b2c3d4e5f6...", + "url": "https://server.example.com/packages/app-v2.0.0.apk", + "packageName": "com.example.myapp", + "fileSize": 50000000, + "format": "apk", + "platform": 4, + "productId": "2d974e2a-31e6-4887-9bb1-b4689e98c77a", + "isForcibly": false, + "releaseDate": "2026-06-14T12:00:00.0000000Z" +} +``` + +### How downstream consumes it + +- Import or upload the generated version record JSON to your update server (e.g. GeneralSpacestation). +- When the client (`GeneralUpdate.Avalonia.Android` or `GeneralUpdate.Maui.Android`) queries the server for version info, the returned data structure maps to the version record's fields. +- The client uses the `hash` field for SHA256 integrity verification after downloading the APK. + +### Supported AndroidManifest fields + +| Manifest attribute | Field | Description | +|-------------------|-------|-------------| +| `package` | `PackageName` | App package name (unique identifier) | +| `android:versionName` | `VersionName` | Display version (e.g. `2.0.0`) | +| `android:versionCode` | `VersionCode` | Internal version code (integer) | + +--- + ## Recommended release workflow -This sequence chains the six modules into a complete release pipeline: +### Desktop update + +This sequence chains the Patch, OSS, Config, and Simulation modules together for desktop app releases: ``` ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ @@ -323,6 +418,22 @@ This sequence chains the six modules into a complete release pipeline: 5. **Simulation**: select the old version directory and patch ZIP, confirm PASS. 6. **Ship**: upload patch ZIP and OSS manifest to production. +### Mobile update + +Use the Mobile module for Android app releases: + +``` +┌────────────┐ ┌──────────┐ ┌──────────┐ +│ Build APK/ │ -> │ Mobile │ -> │ Ship to │ +│ AAB │ │ parse & │ │ update │ +│ artifact │ │ upload │ │ server │ +└────────────┘ └──────────┘ └──────────┘ +``` + +1. **Build artifact**: Build `.apk` / `.aab` in CI or locally. +2. **Mobile**: Select file or project, auto-parse metadata, compute SHA256, upload to server, export version record. +3. **Ship**: Confirm upload success; clients check for updates via `GeneralUpdate.Avalonia.Android` or `GeneralUpdate.Maui.Android`. + --- ## FAQ @@ -344,4 +455,6 @@ This sequence chains the six modules into a complete release pipeline: - [GeneralUpdate.Core](../doc/GeneralUpdate.Core): Client/Upgrade main update flow - [GeneralUpdate.Differential](../doc/GeneralUpdate.Differential): Differential algorithm Clean/Dirty modes - [GeneralUpdate.Extension](../doc/GeneralUpdate.Extension): Extension install and version management +- [GeneralUpdate.Avalonia.Android](../doc/GeneralUpdate.Avalonia.Android): Avalonia Android APK update component +- [GeneralUpdate.Maui.Android](../doc/GeneralUpdate.Maui.Android): MAUI Android APK update component - [Beginner cookbook](./Beginner%20cookbook): Complete end-to-end update walkthrough diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/Maui Android cookbook.md b/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/Maui Android cookbook.md new file mode 100644 index 0000000..ffb1393 --- /dev/null +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/Maui Android cookbook.md @@ -0,0 +1,225 @@ +--- +sidebar_position: 5 +title: MAUI Android Update Quickstart +--- + +# GeneralUpdate.Maui.Android Quickstart + +This guide is for developers who need to integrate Android APK auto-update into their .NET MAUI applications. The goal is to complete the "check update → download APK → verify SHA256 → launch installer" workflow with minimal code. + +:::info Prerequisites +This guide assumes you already have a .NET MAUI project configured for Android target framework. If not, please refer to the [.NET MAUI documentation](https://learn.microsoft.com/dotnet/maui/) first. +::: + +## Update Flow + +``` +① Version Check ② Download APK ③ Install +┌──────────┐ ┌──────────┐ ┌──────────────┐ +│ Client │──POST──→ │ Server │ │ Android │ +│ (MAUI) │←─JSON─── │(Update │ │ Package │ +└────┬─────┘ │ Service) │ │ Installer │ + │ └────┬─────┘ └──────┬───────┘ + │ Server returns pkg info │ + │ ←────────────────────│ │ + │ │ │ + │ GET /packages/app-v2.0.0.apk │ + │ ────────────────────→│ │ + │ APK file (Range supported) │ + │ ←────────────────────│ │ + │ │ │ + │ After SHA256 verification │ + │ launch Package Installer ────────────────→ │ +``` + +## Phase 1: Environment Setup + +### Requirements + +| Item | Requirement | Verify | +| --- | --- | --- | +| .NET SDK | 10.0+ | `dotnet --version` | +| MAUI workload | Installed | `dotnet workload list` (should include `maui`) | + +### NuGet Package + +```bash +dotnet add package GeneralUpdate.Maui.Android +``` + +## Phase 2: FileProvider Configuration + +Add to `Platforms/Android/AndroidManifest.xml`: + +```xml + + + + + +``` + +Create `Platforms/Android/Resources/xml/file_paths.xml`: + +```xml + + + + +``` + +## Phase 3: Write Update Code + +### Approach A: DI Container Integration (Recommended) + +In `MauiProgram.cs`: + +```csharp +using GeneralUpdate.Maui.Android.Services; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => { }); + + // Register update service + builder.Services.AddGeneralUpdateMauiAndroid(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + return builder.Build(); + } +} +``` + +In ViewModel: + +```csharp +using GeneralUpdate.Maui.Android.Abstractions; +using GeneralUpdate.Maui.Android.Models; + +public class MainViewModel +{ + private readonly IAndroidBootstrap _bootstrap; + + public MainViewModel(IAndroidBootstrap bootstrap) + { + _bootstrap = bootstrap; + _bootstrap.AddListenerDownloadProgressChanged += (_, args) => + { + MainThread.BeginInvokeOnMainThread(() => + { + OnProgressChanged?.Invoke(this, args.Statistics.ProgressPercentage); + }); + }; + } + + public async Task CheckAndUpdateAsync() + { + var package = new UpdatePackageInfo + { + Version = "2.0.0", + DownloadUrl = "http://10.0.2.2:5000/packages/app-v2.0.0.apk", + Sha256 = "server-provided-sha256-hash", + PackageSize = 50_000_000, + ApkFileName = "app-v2.0.0.apk" + }; + + var options = new UpdateOptions + { + CurrentVersion = "1.0.0", + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = "com.mycompany.myapp.fileprovider" + } + }; + + var checkResult = await _bootstrap.ValidateAsync(package, options, CancellationToken.None); + if (!checkResult.IsUpdateAvailable) + { + await Shell.Current.DisplayAlert("Info", "Already up to date.", "OK"); + return; + } + + var result = await _bootstrap.ExecuteUpdateAsync(package, options, CancellationToken.None); + + if (result.IsSuccess) + await Shell.Current.DisplayAlert("Update", "Installer launched. Confirm on device.", "OK"); + else + await Shell.Current.DisplayAlert("Update Failed", result.Message, "OK"); + } + + public event EventHandler? OnProgressChanged; +} +``` + +### Approach B: Direct Creation (No DI) + +```csharp +using GeneralUpdate.Maui.Android; +using GeneralUpdate.Maui.Android.Models; + +var bootstrap = GeneralUpdateBootstrap.CreateDefault(); + +var package = new UpdatePackageInfo +{ + Version = "2.0.0", + DownloadUrl = "http://10.0.2.2:5000/packages/app-v2.0.0.apk", + Sha256 = "server-provided-sha256-hash" +}; + +var options = new UpdateOptions +{ + CurrentVersion = "1.0.0", + InstallOptions = new AndroidInstallOptions + { + FileProviderAuthority = "com.mycompany.myapp.fileprovider" + } +}; + +var result = await bootstrap.ExecuteUpdateAsync(package, options, CancellationToken.None); +Console.WriteLine(result.IsSuccess ? "Update completed." : $"Update failed: {result.Message}"); +``` + +## Phase 4: Start the Server + +```bash +git clone https://github.com/GeneralLibrary/GeneralUpdate-Samples.git +cd GeneralUpdate-Samples/src/Server +dotnet run +``` + +Server runs at `http://localhost:5000`. Use `http://10.0.2.2:5000` from Android emulator to access host localhost. + +## Phase 5: End-to-End Verification + +1. Place APK in `Server/wwwroot/packages/` +2. Update `wwwroot/packages/versions.json` with version info +3. Run the MAUI Android app on emulator/device +4. Observe: Check → Download → Verify → Install + +### Expected Output + +``` +Checking for updates... +Update v2.0.0 found. +Downloading... 45% 1.2MB/s +Downloading... 100% +SHA256 verified. +Installer launched — complete install on device. +``` + +## Next Steps + +- [GeneralUpdate.Maui.Android Component Reference](../doc/GeneralUpdate.Maui.Android): Full API reference and advanced usage +- [MAUI Android Update Sample](https://github.com/GeneralLibrary/GeneralUpdate-Samples/tree/main/UI/MauiUpdate): Complete MAUI Android update sample project diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/_category_.json b/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/_category_.json index cc54074..1a8251b 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/_category_.json +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/quickstart/_category_.json @@ -3,6 +3,6 @@ "position": 1, "link": { "type": "generated-index", - "description": "Quick start guides and hands-on cookbooks." + "description": "Quick start guides and hands-on cookbooks for desktop Android APK auto-update." } } diff --git a/website/i18n/en/docusaurus-plugin-content-docs/current/releaselog/GeneralUpdateReleaselog.md b/website/i18n/en/docusaurus-plugin-content-docs/current/releaselog/GeneralUpdateReleaselog.md index aeaa2b4..2a9d92e 100644 --- a/website/i18n/en/docusaurus-plugin-content-docs/current/releaselog/GeneralUpdateReleaselog.md +++ b/website/i18n/en/docusaurus-plugin-content-docs/current/releaselog/GeneralUpdateReleaselog.md @@ -7,6 +7,18 @@ tags: [log] # 📒Release log +### 📍2026-06-14 + +- **GeneralUpdate.Core**: Fixed critical/high security issues found in code audit — BSDIFF overflow, Zip path traversal, ProcessExit deadlock, hardcoded IPC key, etc.; fixed download pipeline failure not aborting Upgrade launch; backup feature disabled by default; removed UpgradeMode pass-through field; added CVP-first upgrade strategy with automatic chain fallback +- **GeneralUpdate.Differential**: BSDIFF overflow length validation fix +- **GeneralUpdate.Bowl**: Fixed Trace Listener lifecycle issue +- **GeneralUpdate.Drivelution**: Fixed strategy lifecycle issue +- **GeneralUpdate.Extension**: Fixed HubConnection lifecycle and RetryPolicy issues +- **GeneralUpdate.Avalonia.Android**: **First public release** `v0.0.1-beta.9` — Avalonia Android APK auto-update component with version comparison, resumable download, SHA256 verification, and Package Installer orchestration; supports HMAC/Bearer/ApiKey/Basic multi-protocol auth +- **GeneralUpdate.Maui.Android**: **First public release** `v0.0.1-beta.3` — MAUI Android APK auto-update component with `ValidateAsync` + `ExecuteUpdateAsync` two-phase API, DI container integration, HTTP Range resume download, and SHA256 verification +- **GeneralUpdate.Tools**: Added Mobile packaging module with APK/AAB file parsing (auto-extract AndroidManifest metadata), `dotnet publish` project build, SHA256 computation, server upload, and version record export +- **Docs**: Added complete GeneralUpdate.Avalonia.Android and GeneralUpdate.Maui.Android component reference docs (EN/CN); added Avalonia/Maui Android quickstart tutorials; Tools doc updated with Mobile module and dual release pipeline + ### 📍2026-05-20 — v10.5.0-beta.2 - **GeneralUpdate.Core**: Unified `GeneralUpdateBootstrap` entry point, merging original ClientCore and Core capabilities; added `SetSource()` lightweight config entry and `generalupdate.manifest.json` minimal setup; refactored download subsystem into replaceable 5-layer model (Source/Policy/Executor/Pipeline/Orchestrator); extension points increased to 10 replaceable interfaces diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/releaselog/GeneralUpdateReleaselog.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/releaselog/GeneralUpdateReleaselog.md index 18409e2..e05d380 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/releaselog/GeneralUpdateReleaselog.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/releaselog/GeneralUpdateReleaselog.md @@ -7,6 +7,18 @@ tags: [log] # 📒Release log +### 📍2026-06-14 + +- **GeneralUpdate.Core**: 修复代码审计发现的 BSDIFF 溢出、Zip 路径遍历、ProcessExit 死锁、IPC 密钥硬编码等严重/高危安全问题;修复下载管道失败后继续启动 Upgrade 的错误行为;备份功能默认关闭(不再默认备份);移除 UpgradeMode 透传字段;新增跨版本优先更新(CVP-first)策略,自动降级为链式更新 +- **GeneralUpdate.Differential**: BSDIFF 溢出长度校验修复 +- **GeneralUpdate.Bowl**: 修复 Trace Listener 生命周期问题 +- **GeneralUpdate.Drivelution**: 修复策略生命周期问题 +- **GeneralUpdate.Extension**: 修复 HubConnection 生命周期和 RetryPolicy 问题 +- **GeneralUpdate.Avalonia.Android**: **首个公开发布** `v0.0.1-beta.9` — Avalonia Android APK 自动更新组件,提供版本对比、断点续传下载、SHA256 校验和 Package Installer 触发编排;支持 HMAC/Bearer/ApiKey/Basic 多协议认证 +- **GeneralUpdate.Maui.Android**: **首个公开发布** `v0.0.1-beta.3` — MAUI Android APK 自动更新组件,提供 `ValidateAsync` + `ExecuteUpdateAsync` 双阶段 API、DI 容器集成、HTTP Range 断点续传和 SHA256 校验 +- **GeneralUpdate.Tools**: 新增 Mobile 打包模块,支持 APK/AAB 文件解析(自动提取 AndroidManifest 元数据)、`dotnet publish` 项目构建、SHA256 计算、服务端上传和版本记录导出 +- **文档**: 新增 GeneralUpdate.Avalonia.Android 和 GeneralUpdate.Maui.Android 完整组件参考文档(中英文);新增 Avalonia/Maui Android 快速入门教程;Tools 文档补充 Mobile 模块和双发布流水线说明 + ### 📍2026-05-20 — v10.5.0-beta.2 - **GeneralUpdate.Core**: 统一 `GeneralUpdateBootstrap` 入口,合并原 ClientCore 和 Core 能力;新增 `SetSource()` 轻配置入口与 `generalupdate.manifest.json` 极简接入;重构下载子系统为可替换的 5 层模型(Source/Policy/Executor/Pipeline/Orchestrator);扩展点体系增至 10 个可替换接口 diff --git a/website/sidebars.js b/website/sidebars.js index 4a34a09..0631ae4 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -22,6 +22,8 @@ const sidebars = { items: [ 'quickstart/Beginner cookbook', 'quickstart/GeneralUpdate.PacketTool', + 'quickstart/Avalonia Android cookbook', + 'quickstart/Maui Android cookbook', ], }, @@ -37,6 +39,8 @@ const sidebars = { 'doc/GeneralUpdate.Differential', 'doc/GeneralUpdate.Drivelution', 'doc/GeneralUpdate.Extension', + 'doc/GeneralUpdate.Avalonia.Android', + 'doc/GeneralUpdate.Maui.Android', ], },