跳到主要内容

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 容器深度集成,简化接入代码
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.AbstractionsMicrosoft.Extensions.HttpXamarin.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<DownloadStatistics> 回调
完成通知各完成阶段(下载完成、校验完成、安装触发、工作流完成)基础可选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:

字段名数据类型默认值是否必填说明
CurrentVersionstring""当前应用版本号
DownloadDirectorystring?null可选下载目录,为空时自动选择
TemporaryFileExtensionstring".downloading"可选临时下载文件扩展名
DeleteCorruptedPackageOnFailurebooltrue可选校验失败时自动删除损坏包
ProgressReportIntervalTimeSpan500ms可选进度报告间隔
InstallOptionsAndroidInstallOptionsnew()可选安装选项

AndroidInstallOptions:

字段名数据类型默认值是否必填说明
FileProviderAuthoritystring""FileProvider authority,需与 AndroidManifest 一致
MimeTypestring"application/vnd.android.package-archive"可选APK 文件的 MIME 类型

HttpDownloadOptions:

字段名数据类型默认值是否必填说明
SslValidationPolicyISslValidationPolicy?null可选自定义 SSL 证书验证策略
DownloadTimeoutTimeSpan10min可选整体下载超时
ProxyIWebProxy?null可选HTTP 代理
UseProxyboolfalse可选是否启用代理
AuthProviderIHttpAuthProvider?null可选全局 HTTP 认证提供器

UpdatePackageInfo:

字段名数据类型默认值是否必填说明
Versionstring""目标版本号
DownloadUrlstring""APK 下载地址
Sha256string""SHA256 哈希值
PackageSizelong?null可选包大小(字节)
ApkFileNamestring?null可选APK 文件名
ForceUpdateboolfalse可选是否强制更新
VersionNamestring?null可选展示名称
ReleaseNotesstring?null可选发布说明
PublishTimeDateTimeOffset?null可选发布时间
AuthSchemeAuthScheme?null可选认证方案
AuthTokenstring?null可选认证令牌
AuthSecretKeystring?null可选HMAC 密钥
BasicUsernamestring?null可选Basic 用户名
BasicPasswordstring?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 回调事件

事件名称回调参数触发时机使用说明
AddListenerValidateValidateEventArgsPackageInfo版本对比完成,发现可用更新时可用于 UI 展示新版本信息
AddListenerDownloadProgressChangedDownloadProgressChangedEventArgsPackageInfo, StatisticsDownloadProgressInfoProgressPercentage, DownloadedBytes, TotalBytes, BytesPerSecond, RemainingBytes), StatusDescription下载进度更新时通过 IProgress<DownloadStatistics> 回调
AddListenerUpdateCompletedUpdateCompletedEventArgsPackageInfo, StageUpdateCompletionStage), PackagePath各阶段完成时分为下载完成、校验完成、安装触发、工作流完成四个阶段
AddListenerUpdateFailedUpdateFailedEventArgsReason, Message, Exception, PackageInfo任何阶段失败时包含结构化的失败原因

4. 扩展示例(高阶用法)

4.1 组件可扩展能力总览

所有服务可通过 AddGeneralUpdateMauiAndroid() 注册后,通过 DI 容器替换:

接口默认实现说明
IUpdateDownloaderHttpRangeDownloader可恢复 APK 下载
IHashValidatorSha256ValidatorSHA256 哈希校验
IApkInstallerAndroidApkInstallerAPK 安装触发
IUpdateStorageProviderUpdateFileStore文件路径和存储操作
IUpdateLoggerNullUpdateLogger日志记录器
IUpdateBootstrapAndroidBootstrap更新编排(通过 DI 注册)
ISslValidationPolicy通过 HttpDownloadOptions 配置SSL 证书验证策略
IHttpAuthProvider通过 HttpDownloadOptions 或单包配置HTTP 认证提供器

4.2 分场景示例

场景 1:DI 容器集成

【场景说明】在 MAUI 应用中将更新服务注册到 DI 容器,便于管理和测试。

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<App>()
.ConfigureFonts(fonts => { });

// 注册更新服务
builder.Services.AddGeneralUpdateMauiAndroid();

// 注册应用服务
builder.Services.AddSingleton<MainViewModel>();
builder.Services.AddSingleton<MainPage>();

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 认证。

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 容器,直接使用静态工厂创建。

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)

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 基础参数组合示例

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 对接、事件处理和错误分类:

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<UpdateResult> 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<UpdatePackageInfo?> 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<ServerResponse>(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<string>? 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<PackageEntry>? 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 注册方式

// 方式一:使用默认 HttpClient
services.AddGeneralUpdateMauiAndroid();

// 方式二:外部传入 HttpClient(共享连接池)
services.AddGeneralUpdateMauiAndroid(myHttpClient);

// 方式三:IHttpClientFactory 注入(通过 AddHttpClient)
services.AddHttpClient<IUpdateDownloader, HttpRangeDownloader>(client =>
{
client.Timeout = TimeSpan.FromMinutes(10);
});
services.AddSingleton<IAndroidBootstrap, AndroidBootstrap>();
// 其他服务同理...

认证方案

MAUI 组件支持四种认证方案,可在 UpdatePackageInfo 单包粒度配置:

认证方案AuthScheme所需字段
HMAC-SHA256HmacAuthSecretKey
Bearer TokenBearerAuthToken
API KeyApiKeyAuthToken
HTTP BasicBasicBasicUsername + BasicPassword

SSL 策略

策略类名使用场景
系统默认(推荐)null生产环境
允许所有(仅开发)AllowAllSslValidationPolicy自签名证书开发环境
自定义实现 ISslValidationPolicy私有 CA

AndroidManifest 配置

<application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>

file_paths.xmlPlatforms/Android/Resources/xml/file_paths.xml):

<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="cache" path="update/" />
</paths>

平台差异

项目说明
最低 API21(Android 5.0)
安装权限Android 8.0+ 需要 CanRequestPackageInstalls() 检查
FileProvider必须通过 FileProvider 向系统安装器传递 APK 文件 URI

相关资源