目录
- 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-path
name="my_download"
path="Download" />
<external-path
name="."
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">
<LinearLayout
android:id="@+id/titleBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/txtStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="状态"
android:textSize="10sp"
android:textStyle="normal" />
<ProgressBar
android: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);
}
//下载APP
private 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 的使用