目录
- 1、具体的效果
- 2、代码实现
- 2.1 基本原理
- 2.2 开发环境
- 2.3 具体代码
- 2.3.1 基本设置
- 2.3.2 系统的权限授予
- 2.3.3 进度条的layout文件
- 2.3.4 核心的升级文件
- 3、代码下载
- 4、知识点
- 5、参考文献
1、具体的效果
有事需要在程序内集成自动更新的功能,网上找了下,改改适配下Xamarin.Android
,效果如下
2、代码实现
2.1 基本原理
这个功能本质上,就是使用一个Intent
打开一个apk
文件进行预览。Android
系统遇到预览apk
文件时,就会弹出“是否进行安装更新”这类的安装框。
2.2 开发环境
VS2022,.NET7,Xamarin.Android、实体手机的Android版本:11
2.3 具体代码
2.3.1 基本设置
1、允许访问http
为了安全,从Android 7.0之后,不允许直接访问http的资源,因为我们会把安装包放在http的网络环境中,因此需要进行一个设置:在AndroidManifest.xml
中application
节点中,直接添加android:usesCleartextTraffic="true"
即可。Android访问http的方案说明
2、设置FileProvider
同样,为了安全,在Android7.0之后,系统安装APP必须使用FileProvider
,因此需要在AndroidManifest.xml
中进行配置provider
3、权限设置
为了能够下载、存放、读取安装包,需要一系列的权限。需要在AndroidManifest.xml
中进行配置
因此最终的配置文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.2" package="com.updateapp" android:installLocation="auto"><uses-sdk android:minSdkVersion="28" android:targetSdkVersion="33" /><!--为了能够安装apk文件,需要下面的一系列授权--><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /><uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /><application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true"> <!--这句话是为了可以访问http的资源--><!--下面的配置,是为了设置FileProvider,其中用到了file_paths配置文件,具体如下--><provider android:name="androidx.core.content.FileProvider" android:authorities="com.updateapp.fileprovider" android:exported="false" android:grantUriPermissions="true"><meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"></meta-data></provider></application>
</manifest>
在Resources
文件下创建xml
文件夹,并创建file_paths.xml
配置文件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android"><!--安装包文件存储路径--><external-files-pathname="my_download"path="Download" /><external-pathname="."path="." />
</paths>
以上,就是第一步,程序的基本配置
2.3.2 系统的权限授予
除了AndroidManifest.xml
中进行配置权限外,还需要进行权限的程序判定及授权
protected override void OnCreate(Bundle savedInstanceState)
{base.OnCreate(savedInstanceState);Xamarin.Essentials.Platform.Init(this, savedInstanceState);SetContentView(Resource.Layout.activity_main);Toolbar toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);SetSupportActionBar(toolbar);FloatingActionButton fab = FindViewById<FloatingActionButton>(Resource.Id.fab);fab.Click += FabOnClick;//版本跟踪,这个是和Android不一样的地方VersionTracking.Track();//初始化自动升级的功能autoUpdater=new AutoUpdater(this);try{//6.0之后才能使用动态授权if(Build.VERSION.SdkInt>=BuildVersionCodes.M){string[] permissions ={Manifest.Permission.ReadExternalStorage,Manifest.Permission.WriteExternalStorage,Manifest.Permission.AccessWifiState,Manifest.Permission.Internet};List<string> permissionList = new List<string>();for (int i = 0; i < permissions.Length; i++){if(ActivityCompat.CheckSelfPermission(this, permissions[i])!=Permission.Granted){ permissionList.Add(permissions[i]);}}//if(permissionList.Count==0){//更新程序autoUpdater.checkUpdate();}else{//获取授权ActivityCompat.RequestPermissions(this, permissions, 100);}}}catch(Exception e){Toast.MakeText(this,"发生异常:"+e.Message,ToastLength.Long).Show();}
}public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);bool checkPermissionFlag = true;if (requestCode == 100){for(int i = 0; i < permissions.Length; i++){if (grantResults[i]== Permission.Granted){checkPermissionFlag = checkPermissionFlag && true;}else{checkPermissionFlag = checkPermissionFlag && false;}}if(!checkPermissionFlag){//授权程序Snackbar.Make(View.Inflate(this,Resource.Id.activity_main_layout,null),"需要授权",Snackbar.LengthIndefinite).SetAction("ok",new Action<View>(delegate (View obj){ActivityCompat.RequestPermissions(this, permissions, 100);})).Show();}else{//更新程序Toast.MakeText(this, "授权后,可以进行更新程序啦!", ToastLength.Long).Show();autoUpdater.checkUpdate();}}base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
2.3.3 进度条的layout文件
在layout文件夹中添加progress.xml
文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayoutandroid:id="@+id/titleBar"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><TextViewandroid:id="@+id/txtStatus"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="状态"android:textSize="10sp"android:textStyle="normal" /><ProgressBarandroid:id="@+id/progress"style="?android:attr/progressBarStyleHorizontal"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_toLeftOf="@id/txtStatus" /></LinearLayout>
</LinearLayout>
2.3.4 核心的升级文件
using Android.App;
using Android.Content;
using Android.Net;
using Android.OS;
using Android.Runtime;
using Android.Systems;
using Android.Util;
using Android.Views;
using Android.Widget;
using Java.IO;
using Java.Net;
using Java.Util.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Contexts;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xamarin.Essentials;
using Context = Android.Content.Context;
using Environment = Android.OS.Environment;namespace UpdateApp
{public class AutoUpdater{private Android.App.AlertDialog confirmDialog = null; //确认是否下载的对话框private Android.App.AlertDialog loadingDialog = null; //正在下载的对话框public MainActivity mainActivity;private UpdateHandler updateHandler;// 保存APK的文件名private static string saveFileName = "my.apk";private static File apkFile;// 进度条与通知UI刷新的handler和msg常量public ProgressBar mProgress;public TextView txtStatus;public int progress;// 当前进度public AutoUpdater(MainActivity activity) {mainActivity = activity;updateHandler = new UpdateHandler(this);apkFile =new File(mainActivity.GetExternalFilesDir(Environment.DirectoryDownloads), saveFileName);}//主方法public void checkUpdate(){//新开启一个线程,进行下载及逻辑判断Task.Run(() => {//获取本地的版本名称(一般而言就是1.0、1.1、1.2的纯数字)string localVersionName = VersionTracking.CurrentVersion;//获取服务器的版本string remoteServerVersion = "2.2"; //远程获取服务器上最新版本,这儿省事儿了,直接默认取了一个较大的值if (Convert.ToDouble(localVersionName)< Convert.ToDouble(remoteServerVersion)){//启动升级的界面updateHandler.SendEmptyMessage((int)UpdateStatusEnum.BeginLoad);}});}//弹框进行下载public void ShowUpdateDialog(){Android.App.AlertDialog.Builder builder = null;builder = new Android.App.AlertDialog.Builder(mainActivity);confirmDialog = builder.SetTitle("软件版本更新").SetMessage("有最新的软件包,请下载并安装!").SetPositiveButton("立即下载", (s, e) => { //确定按钮及内部方法ShowDownloadDialog();confirmDialog.Dismiss();}).SetNegativeButton("以后再说", (s, e) => { //界面上的关闭按钮及方法confirmDialog.Dismiss();}).Create();confirmDialog.Show();}//弹出确认下载的进度条的内容private void ShowDownloadDialog(){CancellationTokenSource cts = new CancellationTokenSource();CancellationToken cancellationToken = cts.Token;Android.App.AlertDialog.Builder builder = null;builder = new Android.App.AlertDialog.Builder(mainActivity);View view = mainActivity.LayoutInflater.Inflate(Resource.Layout.progress, null, false);mProgress = view.FindViewById<ProgressBar>(Resource.Id.progress);txtStatus = view.FindViewById<TextView>(Resource.Id.txtStatus);loadingDialog = builder.SetView(view).SetTitle("正在更新").SetNegativeButton("取消下载", (s, e) => { //界面上的关闭按钮及方法cts.Cancel();}).Create();loadingDialog.Show();DownloadApk(cancellationToken);}//下载APPprivate void DownloadApk(CancellationToken cancellationToken){Task.Run(() => {try { URL url = new URL(@"http://xxx/xxx/xxx/com.updateapp.apk");//apk的网络地址URLConnection conn = url.OpenConnection();conn.Connect();int length = conn.ContentLength;System.IO.Stream ins = conn.InputStream;FileOutputStream fos = new FileOutputStream(apkFile);int count = 0;byte[] buf = new byte[1024];while (!cancellationToken.IsCancellationRequested){int numread = ins.Read(buf);count += numread;progress = (int)(((float)count / length) * 100);//下载进度Message message = new Message();message.What = (int)UpdateStatusEnum.Loading;Bundle extras = new Bundle();extras.PutInt("progress", progress);message.Data = extras;updateHandler.SendMessage(message);if (numread <= 0){Message msg = new Message();//下载完成msg.What = (int)UpdateStatusEnum.Finish; extras.PutInt("progress", 100);msg.Data = extras;updateHandler.SendMessage(msg);//关闭下载框if(loadingDialog!=null) loadingDialog.Dismiss(); break;}fos.Write(buf, 0, numread);}fos.Close();ins.Close();}catch (System.OperationCanceledException el){Log.Info("info", "用户取消了操作!"+el.Message);}catch (AggregateException e){foreach (Exception ex in e.InnerExceptions){Log.Info("info", "发生异常!" + ex.Message);}}}, cancellationToken);}public void installAPK(){try{if(!apkFile.Exists()){Toast.MakeText(mainActivity, "下载的文件不存在!", ToastLength.Short).Show();return;}//这儿是整个的核心Intent intent = new Intent();intent.SetAction(Intent.ActionView);intent.AddFlags(ActivityFlags.NewTask);intent.AddFlags(ActivityFlags.GrantReadUriPermission);intent.AddFlags(ActivityFlags.GrantWriteUriPermission);if (Build.VERSION.SdkInt >=Android.OS.BuildVersionCodes.N){string packageName = mainActivity.ApplicationContext.PackageName;string authority = new StringBuilder(packageName).Append(".fileprovider").ToString();Android.Net.Uri apkUri = FileProvider.GetUriForFile(mainActivity, authority, apkFile);intent.SetDataAndType(apkUri, "application/vnd.android.package-archive");}else{intent.SetDataAndType(Android.Net.Uri.FromFile(apkFile), "application/vnd.android.package-archive");}mainActivity.StartActivity(intent);}catch (Exception ex){Toast.MakeText(mainActivity, "安装installAPK发生异常"+ex.Message, ToastLength.Short).Show();}}}//状态枚举 public enum UpdateStatusEnum:int{ BeginLoad=1,Loading=2,Finish=3}//Handler事件public class UpdateHandler : Android.OS.Handler{private WeakReference<AutoUpdater> weakReference;[Obsolete]public UpdateHandler(AutoUpdater autoUpdater){weakReference = new WeakReference<AutoUpdater>(autoUpdater);}public override void HandleMessage(Message msg){AutoUpdater targetActivity;bool isGetSuccess = weakReference.TryGetTarget(out targetActivity);if (isGetSuccess){switch (msg.What){case (int)UpdateStatusEnum.BeginLoad:targetActivity.ShowUpdateDialog();break;case (int)UpdateStatusEnum.Loading://获取状态数据,并进行展示int progress = msg.Data.GetInt("progress");targetActivity.txtStatus.SetText(progress + "%",TextView.BufferType.Normal);targetActivity.mProgress.SetProgress(progress, true);break;case (int)UpdateStatusEnum.Finish:Toast.MakeText(targetActivity.mainActivity, "下载完毕", ToastLength.Long).Show();targetActivity.installAPK();break;default:break;}}base.HandleMessage(msg);}}}
上面是这个的核心
3、代码下载
代码下载
4、知识点
1、Handler的用法,C#与Java还是不同的,这里涉及到的知识点是匿名类和委托。C#的匿名类是一个field的集合,不能包含方法
2、Android中更新应用的逻辑
每个 Android 应用均有一个唯一的应用 ID,像 Java 或 Kotlin 软件包名称一样,例如 com.example.myapp。此 ID 可以作为每个应用在设备上的唯一标识。Android 设备一次只能安装一个具有指定应用 ID 的应用。
为了让 Android 平台接受更新,更新必须满足以下条件:
应用更新的应用 ID 必须与已安装应用的应用 ID 相同。
应用更新的签名证书必须与已安装应用的签名证书相同,或者必须包含有效的 proof-of-rotation。
应用更新的版本代码必须高于或等于已安装应用的版本代码。
在某些情况下,用户可能需要接受更新。
请注意,如果多个更新具有相同的签名证书并且具有相同或更高的版本代码,Android 内部并没有防范措施能够阻止不同的安装程序更新应用。
如要安装不符合上述条件的应用,用户必须先卸载当前已安装的版本,而卸载操作会清除设备上的所有应用数据。
5、参考文献
主要参考了前两个
1、Android App自动安装
2、Android APP 自动更新实现(适用Android9.0)
3、【Android】APP检测版本升级更新、apk安装
4、Andrioid FileProvider在Xamarin.Forms中的使用
5、Xamarin.Android 中 Handler 的使用