MAUI Blazor学习18-自动升级

MAUI Blazor学习18-自动升级

 

MAUI Blazor系列目录

  1. MAUI Blazor学习1-移动客户端Shell布局 - SunnyTrudeau - 博客园 (cnblogs.com)
  2. MAUI Blazor学习2-创建移动客户端Razor页面 - SunnyTrudeau - 博客园 (cnblogs.com)
  3. MAUI Blazor学习3-绘制ECharts图表 - SunnyTrudeau - 博客园 (cnblogs.com)
  4. MAUI Blazor学习4-绘制BootstrapBlazor.Chart图表 - SunnyTrudeau - 博客园 (cnblogs.com)
  5. MAUI Blazor学习5-BLE低功耗蓝牙 - SunnyTrudeau - 博客园 (cnblogs.com)
  6. MAUI Blazor学习6-扫描二维码 - SunnyTrudeau - 博客园 (cnblogs.com)
  7. MAUI Blazor学习7-实现登录跳转页面 - SunnyTrudeau - 博客园 (cnblogs.com)
  8. MAUI Blazor学习8-支持多语言 - SunnyTrudeau - 博客园 (cnblogs.com)
  9. MAUI Blazor学习9-VS Code开发调试MAUI入门 - SunnyTrudeau - 博客园 (cnblogs.com)
  10. MAUI Blazor学习10-BarcodeScanner扫描二维码 - SunnyTrudeau - 博客园 (cnblogs.com)
  11. MAUI Blazor学习11-百度地图定位 - SunnyTrudeau - 博客园 (cnblogs.com)
  12. MAUI Blazor学习12-文件另存为 - SunnyTrudeau - 博客园 (cnblogs.com)
  13. MAUI Blazor学习13-打开文件 - SunnyTrudeau - 博客园 (cnblogs.com)
  14. MAUI Blazor学习14-选择目录 - SunnyTrudeau - 博客园 (cnblogs.com)
  15. MAUI Blazor学习15-采用html2pdf.js生成pdf - SunnyTrudeau - 博客园 (cnblogs.com)
  16. MAUI Blazor学习16-连续按BACK退出APP - SunnyTrudeau - 博客园 (cnblogs.com)
  17. MAUI Blazor学习17-NavigationLock阻止页面回退 - SunnyTrudeau - 博客园 (cnblogs.com)

 

消费类的APP一般通过应用市场发行,便于广大消费者下载和升级。专业工具类的APP可以由制造商自行发布给特定范围的终端用户,APP需要具备自动检测新版本,自动升级的功能,便于管控。网上有很多介绍安卓APP自动升级方案的文章,大致如下:

1,部署一个云服务器,存放新版APP安装包,可以下载文件;

2APP访问云服务器,检测最新版本和当前版本,如果发现有版本,询问用户是否升级;

3APP从云服务器下载安装包,调用安卓系统的安装APP功能;

 

新建APP升级文件云服务

基于MaBlaApp解决方案,新建AppFileLib类库。新建APP文件信息记录,描述APP升级文件的关键信息。

D:\Software\gitee\mauiblazorapp\AppFileLib\AppFileInfo.cs

 

namespace AppFileLib;

/// <summary>
/// APP文件信息
/// </summary>
/// <param name="AppName">APP名称</param>
/// <param name="NumericVersion">数字版本号,例如2,用于比较,等于Microsoft.Maui.ApplicationModel.AppInfo.BuildString</param>
/// <param name="DisplayVersion">显示版本号,例如1.0.2,用于显示,等于Microsoft.Maui.ApplicationModel.AppInfo.VersionString</param>
/// <param name="Description">说明</param>
/// <param name="HashCode">哈希码</param>
public record AppFileInfo(string AppName, int NumericVersion, string DisplayVersion, string Description, string HashCode);

 

新建Asp.Net Core Web Api项目AppFileServer。提供查询APP新版本,下载APP文件的控制器。

D:\Software\gitee\mauiblazorapp\AppFileServer\Controllers\AppFileInfoController.cs

/// <summary>
/// APP文件信息
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class AppFileInfoController : ControllerBase
{
    private readonly IWebHostEnvironment _webHostEnvironment;
    private readonly ILogger<AppFileInfoController> _logger;

    public AppFileInfoController(IWebHostEnvironment webHostEnvironment, ILogger<AppFileInfoController> logger)
    {
        _webHostEnvironment = webHostEnvironment;
        _logger = logger;
    }

    /// <summary>
    /// 获取APP文件信息
    /// </summary>
    /// <returns></returns>
    [HttpGet("{appName}/FileInfo")]
    public async Task<ActionResult<AppFileInfo?>> GetFileInfo([FromRoute] string appName)
    {
        string filePath = Path.Combine(_webHostEnvironment.WebRootPath, appName, "fileinfo.json");

        if (!System.IO.File.Exists(filePath))
            return NotFound();

        string fileInfoJson = await System.IO.File.ReadAllTextAsync(filePath);

        var fileInfo = System.Text.Json.JsonSerializer.Deserialize<AppFileInfo>(fileInfoJson);

        return fileInfo;
    }

    /// <summary>
    /// 获取APP升级文件
    /// </summary>
    /// <returns></returns>
    [HttpGet("{appName}/AppFile")]
    public ActionResult GetAppFile([FromRoute] string appName)
    {
        string fileName = "app.apk";
        string filePath = Path.Combine(_webHostEnvironment.WebRootPath, appName, fileName);

        if (!System.IO.File.Exists(filePath))
            return NotFound();

        return File(new FileStream(filePath, FileMode.Open), "application/octet-stream", fileName);
    }
}

AppFileServer目录新建wwwroot目录,下边再新建MaBlaApp目录,用来存放MaBlaApp升级文件和版本说明文件。

新建fileinfo.json,描述APP文件信息,跟AppFileInfo记录匹配。

D:\Software\gitee\mauiblazorapp\AppFileServer\wwwroot\MaBlaApp\fileinfo.json

{
  "AppName": "MaBlaApp",
  "NumericVersion": 2,
  "DisplayVersion": "1.0.2",
  "Description": "增加检测升级APP功能",
  "HashCode": "xxx"
}

AppFileServer侦听端口改为8500,本机调试运行,通过swagger测试Web Api接口,确认可用。

D:\Software\gitee\mauiblazorapp\AppFileServer\Properties\launchSettings.json

    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "https://localhost:8500",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },

 

MaBlaApp项目增加检查APP升级文件功能

MaBlaApp项目引用AppFileLib类库。

新建升级软件服务接口IUpgradeAppService

D:\Software\gitee\mauiblazorapp\MaBlaApp\Interfaces\IUpgradeAppService.cs

using AppFileLib;

namespace MaBlaApp.Interfaces;

/// <summary>
/// 升级软件服务接口
/// </summary>
public interface IUpgradeAppService
{
    /// <summary>
    /// 检查新版软件
    /// </summary>
    /// <param name="appName">APP名称</param>
    /// <returns></returns>
    Task<(AppFileInfo?, string Msg)> CheckNewVersionAsync(string appName);

    /// <summary>
    /// 检查获取安装APP权限
    /// </summary>
    /// <returns></returns>
    Task<bool> CheckAndRequestInsatllAppPermission();

    /// <summary>
    /// 下载安装文件
    /// </summary>
    /// <param name="appName">APP名称</param>
    /// <param name="appHashCode">哈希码</param>
    /// <param name="processHandler">进度条处理方法</param>
    /// <returns></returns>
    Task<(bool Success, string Err)> DownloadFileAsync(string appName, string appHashCode, Action<long, long> processHandler);

    /// <summary>
    /// 安装新版软件
    /// </summary>
    Task InstallNewVersion();
}

在安卓平台目录下面实现UpgradeAppService

D:\Software\gitee\mauiblazorapp\MaBlaApp\Platforms\Android\UpgradeAppService.cs

 

using Android.Content;
using Android.OS;
using AppFileLib;
using MaBlaApp.Interfaces;
using System.Net.Http.Json;
using System.Security.Cryptography;

namespace MaBlaApp;

/// <summary>
/// 升级软件服务接口
/// </summary>
public class UpgradeAppService : IUpgradeAppService
{
    readonly string ApkName = "com.companyname.mablaapp.apk";

    //来自AndroidManifest.xml
    readonly string FileAuthorities = "com.companyname.mablaapp.fileprovider";

    private readonly IHttpClientFactory _httpClientFactory;

    public UpgradeAppService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    /// <summary>
    /// 检查新版软件
    /// </summary>
    /// <param name="appName">APP名称</param>
    /// <returns></returns>
    public async Task<(AppFileInfo?, string Msg)> CheckNewVersionAsync(string appName)
    {
        try
        {
            //获取当前版本号
            //Microsoft.Maui.ApplicationModel.VersionTracking.CurrentVersion=csproj项目配置文件ApplicationDisplayVersion=1.0.1
            //Microsoft.Maui.ApplicationModel.AppInfo.VersionString=1.0.1,AppInfo.BuildString=1
            var currentVersion = int.Parse(AppInfo.BuildString);

            string url = $"api/AppFileInfo/{appName}/FileInfo";

            System.Diagnostics.Debug.WriteLine($"检查更新, url={url}");

            var _client = _httpClientFactory.CreateClient("AppFileClient");

            //获取服务器最新软件版本
            var latestVersion = await _client.GetFromJsonAsync<AppFileInfo>(url);

            System.Diagnostics.Debug.WriteLine($"检查更新, currentVersion={currentVersion}, latestVersion={latestVersion?.NumericVersion}");

            if (latestVersion is null)
            {
                return (null, "获取服务器软件信息失败");
            }

            //如果服务器版本号跟当前版本号不一致,返回新版软件信息
            if (currentVersion != latestVersion!.NumericVersion)
                return (latestVersion, "");
            else
                return (null, "已经是最新版本");
        }
        catch (Exception ex)
        {
            string err = $"检查更新出错, {ex}";
            System.Diagnostics.Debug.WriteLine(err);
            return (null, err);
        }
    }

    #region 获取安装APP权限

    /// <summary>
    /// 检查获取安装APP权限
    /// </summary>
    /// <returns></returns>
    public async Task<bool> CheckAndRequestInsatllAppPermission()
    {
        if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
        {
            bool canRequestPackageInstalls = Android.App.Application.Context.PackageManager.CanRequestPackageInstalls();

            var status = await Permissions.CheckStatusAsync<InstallAppPermissions>();

            //VS2022安卓模拟器,[0:] 获取安装未知应用权限, canRequestPackageInstalls=True, RequestInstallPackagesStatus=Denied
            System.Diagnostics.Debug.WriteLine($"获取安装未知应用权限, canRequestPackageInstalls={canRequestPackageInstalls}, RequestInstallPackagesStatus={status}");

            if (!canRequestPackageInstalls)
            {
                //var status = await Permissions.CheckStatusAsync<InstallAppPermissions>();

                //if (status == PermissionStatus.Granted)
                //    return true;

                //没有触发用户同意,弹出允许安装未知来源软件页面
                //status = await Permissions.RequestAsync<InstallAppPermissions>();

                requestInstallPermission();

                if (status == PermissionStatus.Granted)
                    return true;
            }
            else
            {
                return true;
            }
        }

        return false;
    }

    //public const int REQUEST_CODE_INSTALL_PERMISSION = 100;

    private void requestInstallPermission()
    {
        if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
        {
            //Intent intent = new Intent(Android.Content.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Android.Net.Uri.Parse("package:" + Android.App.Application.Context.PackageName));
            //Intent intent = new Intent(Intent.ExtraNotUnknownSource, Android.Net.Uri.Parse("package:" + Android.App.Application.Context.PackageName));
            Intent intent = new Intent(Android.Provider.Settings.ActionManageUnknownAppSources, Android.Net.Uri.Parse("package:" + Android.App.Application.Context.PackageName));
            //Android.App.Application.Context.StartActivityForResult(intent, REQUEST_CODE_INSTALL_PERMISSION);
            //registerForActivityResult(intent, REQUEST_CODE_INSTALL_PERMISSION);
            intent.AddFlags(ActivityFlags.NewTask);
            intent.AddFlags(ActivityFlags.ResetTaskIfNeeded);

            Android.App.Application.Context.StartActivity(intent);
        }
    }

    #endregion

    /// <summary>
    /// 下载安装文件
    /// </summary>
    /// <param name="appName">APP名称</param>
    /// <param name="appHashCode">哈希码</param>
    /// <param name="processHandler">进度条处理方法</param>
    /// <returns></returns>
    public async Task<(bool Success, string Err)> DownloadFileAsync(string appName, string appHashCode, Action<long, long> processHandler)
    {
        try
        {
            var _client = _httpClientFactory.CreateClient("AppFileClient");

            string url = $"api/AppFileInfo/{appName}/AppFile";
            var req = new HttpRequestMessage(HttpMethod.Get, url);

            //获取到http头就开始处理
            var response = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);

            var fileLen = response.Content.Headers.ContentLength;

            var responseStream = await response.Content.ReadAsStreamAsync();

            var filePath = Path.Combine(FileSystem.AppDataDirectory, ApkName);
            System.Diagnostics.Debug.WriteLine($"下载文件保存路径: {filePath}");
            //[0:] 下载文件保存路径: /data/user/0/com.companyname.mablaapp/files/com.companyname.mablaapp.apk

            using var fileStream = new FileStream(filePath, FileMode.Create);
            var buffer = new byte[10240];
            var downloadLen = 0;
            int readLen = 0;

            while ((readLen = await responseStream.ReadAsync(buffer)) != 0)
            {
                // 写入到文件
                await fileStream.WriteAsync(buffer, 0, readLen);

                downloadLen += readLen;
                processHandler(downloadLen, fileLen!.Value);
                System.Diagnostics.Debug.WriteLine($"下载文件, downloadLen={downloadLen:N0}, fileLen={fileLen!.Value:N0}");
            }

            //检查文件哈希吗
            string fileHash = GetFileHash(fileStream);
            if (fileHash == appHashCode)
            {
                return (true, "");
            }
            else
            {
                string err = $"下载文件哈希吗错误, fileHash={fileHash}, appHashCode={appHashCode}";
                System.Diagnostics.Debug.WriteLine(err);
                return (false, err);
            }
        }
        catch (Exception ex)
        {
            string err = $"下载文件出错, {ex}";
            System.Diagnostics.Debug.WriteLine(err);
            return (false, err);
        }
    }

    //计算文件哈希吗
    private string GetFileHash(Stream stream)
    {
        stream.Position = 0;

        using var sha256 = SHA256.Create();
        var hash = sha256.ComputeHash(stream);
        string fileHash = BitConverter.ToString(hash).Replace("-", "").ToLower();

        return fileHash;
    }

    /// <summary>
    /// 安装新版软件
    /// </summary>
    public async Task InstallNewVersion()
    {
        try
        {
            var filePath = Path.Combine(FileSystem.AppDataDirectory, ApkName);

            var intent = new Intent(Intent.ActionView);

            Android.Net.Uri? uri;

            // 判断Android版本
            if (Build.VERSION.SdkInt >= BuildVersionCodes.N)
            {
                //给临时读取权限
                intent.SetFlags(ActivityFlags.GrantReadUriPermission);
                var apkFile = new Java.IO.File(filePath);
                uri = FileProvider.GetUriForFile(Android.App.Application.Context, FileAuthorities, apkFile);
            }
            else
            {
                uri = Android.Net.Uri.FromFile(new Java.IO.File(filePath));
            }

            // 设置显式 MIME 数据类型
            intent.SetDataAndType(uri, "application/vnd.android.package-archive");

            //指定以新任务的方式启动Activity
            intent.AddFlags(ActivityFlags.NewTask);

            System.Diagnostics.Debug.WriteLine($"开始安装软件, filePath={filePath}, uri={uri}, intent={intent}");

            //激活一个新的Activity
            Android.App.Application.Context.StartActivity(intent);

            System.Diagnostics.Debug.WriteLine($"完成安装软件");
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine($"安装软件出错, {ex}");
        }
    }
}

 

添加申请安装APP权限InstallAppPermissions

D:\Software\gitee\mauiblazorapp\MaBlaApp\Platforms\Android\InstallAppPermissions.cs

using Android.OS;

namespace MaBlaApp;

/// <summary>
/// 申请安装APP权限
/// </summary>
public class InstallAppPermissions : Permissions.BasePlatformPermission
{
    public override (string androidPermission, bool isRuntime)[] RequiredPermissions => GetRequiredPermissions();

    //根据安卓平台版本,返回对应的申请权限
    private (string androidPermission, bool isRuntime)[] GetRequiredPermissions()
    {
        //https://blog.51cto.com/u_16213404/9289944
        //《android 11静默安装失败_mob64ca12e9cad4的技术博客_51CTO博客.mhtml》

        var permissions = new List<string>();

        //Android 9 API 28 Pie (Android P) 2018
        //Android 8 API 26 Oreo(Android O) 2017
        if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
        {
            permissions.Add(global::Android.Manifest.Permission.RequestInstallPackages);
        }

        var result = new List<(string androidPermission, bool isRuntime)>();
        foreach (var permission in permissions)
        {
            result.Add((permission, true));
        }

        return result.ToArray();
    }
}

注册UpgradeAppService

D:\Software\gitee\mauiblazorapp\MaBlaApp\MauiProgram.cs

#if ANDROID
        builder.Services.AddSingleton<IUpgradeAppService, UpgradeAppService>();
#endif

        //注册访问升级文件服务器的HttpClient
        builder.Services.AddHttpClient("AppFileClient")
            .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
            {
                ServerCertificateCustomValidationCallback = delegate { return true; }//忽略https证书检查
            })
        //.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://www.myappfile.com:8500"));//发布到手机,访问云服务器
        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://10.0.2.2:8500"));//VS2022调试安卓模拟器

在安卓配置文件添加文件服务声明。

D:\Software\gitee\mauiblazorapp\MaBlaApp\Platforms\Android\AndroidManifest.xml

    <application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true">
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.companyname.mablaapp.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths" />
        </provider>
    </application>

    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

添加provider_paths.xml文件。

D:\Software\gitee\mauiblazorapp\MaBlaApp\Platforms\Android\Resources\xml\provider_paths.xml

<?xml version="1.0" encoding="utf-8" ?>
<resources>
    <paths>
        <root-path name="root" path="" />
        <files-path name="files" path="" />
        <cache-path name="cache" path="" />
        <external-path name="camera_photos" path="" />
        <external-files-path name="external_file_path" path="" />
        <external-cache-path name="external_cache_path" path="" />
    </paths>
</resources>

MaBlaApp项目,添加UpgradeApp.razor页面,点击【检查新版本】菜单可以访问服务器,可以下载新版APP然后启动安装。在实际产品项目中,也可以在APP启动后,在后台访问服务器,检查新版本。推荐采用数字类型的Microsoft.Maui.ApplicationModel.AppInfo.BuildString比较版本号,比较方便,如果采用字符串类型的AppInfo.VersionString,需要把多个小数点分隔的版本号转换为数字再比较,比较麻烦。

D:\Software\gitee\mauiblazorapp\MaBlaApp\Pages\UpgradeApp.razor

 

@page "/upgradeapp"
@using MaBlaApp.Interfaces
@using AppFileLib
@inject IUpgradeAppService UpgradeAppService

<h3>检查升级APP</h3>
<ul class="list-group">
    <li class="list-group-item d-flex justify-content-between">
        <strong>当前显示版本</strong>
        <small>@AppInfo.VersionString</small>
    </li>
    <li class="list-group-item d-flex justify-content-between">
        <strong>当前数字版本</strong>
        <small>@AppInfo.BuildString</small>
    </li>
</ul>

<div class="d-flex justify-content-between m-1">
    <button class="btn btn-primary mx-2 @BtnStartCss" @onclick=CheckNewVersion>检查新版本</button>
</div>
@if (FileInfo is not null)
{
    <ul class="list-group">
        <li class="list-group-item d-flex justify-content-between">
            <strong>最新显示版本</strong>
            <small>@FileInfo.DisplayVersion</small>
        </li>
        <li class="list-group-item d-flex justify-content-between">
            <strong>最新数字版本</strong>
            <small>@FileInfo.NumericVersion</small>
        </li>
    </ul>
}

@if (IsShowProgress)
{
    <div class="m-2">
        <label class="control-label">@ProcessTitle</label>
        <div class="progress" style="height:10px">
            <div class="progress-bar bg-primary" style="width:@ProcessVal"></div>
        </div>
    </div>
}
<label class="control-label m-2">@Msg</label>

@code {

    string BtnStartCss => IsBusy ? "disabled" : "";

    bool IsBusy = false;
    CancellationTokenSource Cts;

    AppFileInfo? FileInfo = null;

    bool IsShowProgress = false;
    string ProcessVal = "";
    string ProcessTitle = "";
    string Msg = "";
    DateTimeOffset LastUpdateProcessTime = DateTimeOffset.Now;

    async void CheckNewVersion()
    {
        //检查获取安装APP权限
        bool hasInsatllAppPermission = await UpgradeAppService.CheckAndRequestInsatllAppPermission();
        if (!hasInsatllAppPermission)
            return;

        IsShowProgress = false;
        Msg = "获取服务器软件信息...";

        await InvokeAsync(() => StateHasChanged());

        //检查更新
        (FileInfo, Msg) = await UpgradeAppService.CheckNewVersionAsync("MaBlaApp");

        await InvokeAsync(() => StateHasChanged());

        if (FileInfo is not null)
        {
            string msg = $"当前版本 V{AppInfo.VersionString}, {Environment.NewLine}检测到新版本 V{FileInfo.DisplayVersion},{Environment.NewLine}说明: {FileInfo.Description}, {Environment.NewLine}是否升级?";

            var confirm = await Application.Current.MainPage.DisplayAlert("信息", msg, "", "");
            if (confirm)
            {
                IsShowProgress = true;

                //下载安装文件
                var result = await UpgradeAppService.DownloadFileAsync(FileInfo.AppName, FileInfo.HashCode, DownloadProgressChanged);

                if (result.Success)
                {
                    //安装新版软件
                    await UpgradeAppService.InstallNewVersion();
                }
                else
                {
                    Msg = result.Err;
                }
            }
        }
        // else
        // {
        //     await Application.Current.MainPage.DisplayAlert("信息", $"当前版本已经是最新版", "确定");
        // }
        await InvokeAsync(() => StateHasChanged());
    }

    private async void DownloadProgressChanged(long downloadLen, long fileLen)
    {
        int currentProcess = (int)(downloadLen * 100 / fileLen);

        if (currentProcess < 100)
        {
            //定期刷新进度
            if (DateTimeOffset.Now.Subtract(LastUpdateProcessTime).TotalSeconds < 1)
                return;

            LastUpdateProcessTime = DateTimeOffset.Now;

            ProcessTitle = $"正在下载软件 {downloadLen:N0}/{fileLen:N0}, 进度{currentProcess}%";
            ProcessVal = $"{currentProcess}%";
        }
        else
        {
            ProcessTitle = $"完成下载软件 {downloadLen:N0}/{fileLen:N0}, 进度{currentProcess}%";
            ProcessVal = $"{currentProcess}%";
        }

        await InvokeAsync(() => StateHasChanged());
    }
}

 

 

发布MaBlaApp安装包

MaBlaApp项目的显示版本号设置为1.0.1,数字版本号设置为1

D:\Software\gitee\mauiblazorapp\MaBlaApp\MaBlaApp.csproj

        <ApplicationDisplayVersion>1.0.1</ApplicationDisplayVersion>

        <ApplicationVersion>1</ApplicationVersion>

 

注意csproj对版本号格式有限制,我本来想设置为

        <ApplicationDisplayVersion>1.0.0.1</ApplicationDisplayVersion>

        <ApplicationVersion>1001</ApplicationVersion>

调试运行没问题,但是发布apk报错

1>C:\Program Files\dotnet\packs\Microsoft.Maui.Resizetizer.Sdk\7.0.101\targets\Microsoft.Maui.Resizetizer.targets(655,9): error : ApplicationDisplayVersion '1.0.0.1' was not a valid 3 part semver version number and/or ApplicationVersion '1001' was not a valid integer.

1>已完成生成项目“MaBlaApp.csproj”的操作 - 失败。

 

发布一个MaBlaAppapk,注意要先把项目改为release模式,把项目属性的android包格式设置为APK,再发布apk

VS2022存档管理器页面,分发apk,选择临时,创建一个秘钥证书,别名mabla,密码mabla1234。选择mabla证书,把apk另存为com.companyname.mablaapp.apk,需要输入证书密码mabla1234,最终把com.companyname.mablaapp.apk文件复制到D:\Software\gitee\mauiblazorapp\AppFileServer\wwwroot\MaBlaApp\app.apk备用。

然后获取app.apkSHA256,进入win11控制台,进入app.apk所在目录,执行命令

D:\myhome-txy\appfileserver\pub\wwwroot\MaBlaApp>certutil -hashfile app.apk SHA256

SHA256 app.apk 哈希:

xxx

 

把哈希码填写到服务端项目的fileinfo.json文件HashCode字段

D:\Software\gitee\mauiblazorapp\AppFileServer\wwwroot\MaBlaApp\fileinfo.json

 

在安卓模拟器调试运行

VS2022自带的安卓模拟器上调试运行MaBlaAppAppFileServer项目,MaBlaApp可以访问localhost地址的AppFileServer,检测APP新版本。

首次安装MaBlaApp运行,在弹出的申请安装未知应用页面打开【允许来自此来源的应用】。再次运行MaBlaApp,就不会弹出这个页面了。

可以下载APP文件。

但是无法安装。

我本来以为是代码有BUG,调试发现MaBlaApp已经具备了安装APP的权限,但是请求安装APP总是拒绝!我觉得非常离谱,后来查阅了一些资料,说是安卓模拟器不支持应用内安装APP,所以,需要用真机来测试。

bool canRequestPackageInstalls = Android.App.Application.Context.PackageManager.CanRequestPackageInstalls();

 

var status = await Permissions.CheckStatusAsync<InstallAppPermissions>();

 

//VS2022安卓模拟器,[0:] 获取安装未知应用权限, canRequestPackageInstalls=True, RequestInstallPackagesStatus=Denied

System.Diagnostics.Debug.WriteLine($"获取安装未知应用权限, canRequestPackageInstalls={canRequestPackageInstalls}, RequestInstallPackagesStatus={status}");

 

在安卓手机调试运行

把AppFileServer通过容器化方式部署到云服务器。AppFileServer添加Dockerfile。编写docker-compose.yml,为了方便调试,通过环境变量配置SSL证书和密码,注意把SSL证书文件放在docker-compose.yml当前目录下。

D:\Software\gitee\mauiblazorapp\AppFileServer\docker-compose.yml

version: '3'

services:

  appfileserver:
    container_name: appfileserver
    image: appfileserverimage
    build:
      context: ./pub
      dockerfile: Dockerfile
    ports:
      - "8500:8500"
    environment:
#      - ASPNETCORE_ENVIRONMENT=Production
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=https://*:8500
      - ASPNETCORE_HTTPS_PORT=8500
#配置SSL证书和密码
      - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/myweb.pfx
      - ASPNETCORE_Kestrel__Certificates__Default__Password=xxx
      - TZ=Asia/Shanghai
    volumes:
      - ./data:/app/data
      - ./myweb.pfx:/https/myweb.pfx
#      - ./aspnetkeys:/root/.aspnet/DataProtection-Keys
    security_opt:
      - "seccomp:unconfined"
    restart: always

 

MaBlaApp项目的服务器地址改为云服务器(比如www.myappfile.com),再次发布一个apk,复制到AppFileServerwwwroot\MaBlaApp\app.apk并部署到云服务器。

D:\Software\gitee\mauiblazorapp\MaBlaApp\MauiProgram.cs

    public static MauiApp CreateMauiApp()

        //注册访问升级文件服务器的HttpClient

        builder.Services.AddHttpClient("AppFileClient")

            .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler

            {

                ServerCertificateCustomValidationCallback = delegate { return true; }//忽略https证书检查

            })

        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://www.myappfile.com:8500"));//发布到手机,访问云服务器

        //.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://10.0.2.2:8500"));//VS2022调试安卓模拟器

 

在安卓手机调试运行MaBlaApp,测试手机是华为鸿蒙,兼容安卓12API 31。测试安装失败了,提示apk签名不一致,因为下载的apkrelaese发布签名的,跟调试运行的debug版本的apk不是一个

release发布的apk直接安装到手机,然后再运行发布版的MaBlaApp,终于成功实现了安装新版APP功能

 

问题小结

1,安卓模拟器不支持应用内安装APP,需要在手机上测试;

2debug模式运行的APP,不支持升级release模式发布的APK,需要在手机上直接运行releaseAPK

 

DEMO代码地址:https://gitee.com/woodsun/mauiblazorapp

 

posted on 2024-10-28 22:18  SunnyTrudeau  阅读(19)  评论(0编辑  收藏  举报