写在前面
之前项目需要实现内部更新功能,看到了Android实现APP在线下载更新这篇文章,对于能够一行代码就集成更新模块很感兴趣,看了下作者的源码,发现和项目需求略有些出入,而且没有做7.0的适配,所以尝试着写了一个更新库,总体思路参考该作者,致谢!
需求与实现代码
项目里的需求主要是这样
- 1.进入APP时进行自动更新检测,同时在设置页可以手动检测更新。
- 2.后台提供版本号和版本名进行选择更新,同时提供最低版本名和版本号,也就是在目前版本低于最低版本时,说明后台API发生了变化,低于最低版本的APP已经不能用了,必须强制更新。
代码完成后写起来是这样的,参考了作者的写法,只是多了一些可选的字段。
UpdateManager.getInstance().init(this)
.compare(UpdateManager.COMPARE_VERSION_NAME)
.downloadUrl("http://aaa.apk")
.downloadTitle("我在下载xxxb了")
.lastestVerCode(0)
.minVerCode(1)
.lastestVerName("1.1")
.update();
大致思路
- 1.更新下载的话,考虑采用官方提供的DownloadManager。之前想用其它下载库,不过app的更新模块也就只有更新版本才会用到,还是不要再引入额外的依赖了,而且DownloadManager用起来还是挺方便的。
- 2.由于存在手动更新的功能,如果当前已经处于更新了,那再调用更新模块的话,需要进行提示;同时,如果是最新版本了,进入APP时不进行提示,但在手动更新时需要,所以考虑采用单例模式和接口回调,将检测结果返回。
- 3.采用注册广播的方式,监听下载完成并自动安装
- 4.主要代码分为UpdateManager和UpdateReceiver,前者负责更新版本对比和下载APK,后者负责下载完成后的操作。
UpdateManager
先在AndroidManifest里进行注册权限
<uses-permission android:name="android.permission.INTERNET"/><!--用于访问网络-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/><!--写入外部存储-->
采用单例进行创建,简化版的构建者模式进行参数设置
public static UpdateManager getInstance() {
if (mInstance == null) {
mInstance = new UpdateManager();
}
return mInstance;
}
/**
* 初始化,必备
*/
public UpdateManager init(Context context) {
this.mContext = context;
mCurrentVerName = LibUtils.getVersionName(mContext);
mCurrentVerCode = LibUtils.getVersionCode(mContext);
return this;
}
/**
* 最新版本名
*/
public UpdateManager lastestVerName(String versionName) {
this.mLastestVerName = versionName;
return this;
}
在update()的方法中,开始进入更新流程,先判断当前是否处于更新下载中,然后进行版本对比
public UpdateManager update() {
if (isUpdating) {
Toast.makeText(mContext, "正在更新中...", Toast.LENGTH_SHORT).show();
return this;
} else {
checkUpdate();
return this;
}
}
/**
* 检查更新
*/
private void checkUpdate() {
// 跳过版本对比,强制更新
if (isForce) {
beginUpdate(true);
if (mListener != null) {
mListener.onCheckResult(RESULT_FORCE);
}
return;
}
switch (mCompare) {
case COMPARE_VERSION_CODE:
compareVerCode();
break;
case COMPARE_VERSION_NAME:
compareVerName();
break;
default:
compareVerCode();
break;
}
}
对比版本后,如果设置了最低版本,同时当前版本又低于最低版本时,提示必须更新,否则退出程序;反之只进行新版本提示,最后进行下载
private void download() {
if (TextUtils.isEmpty(mDownloadUrl)) {
return;
}
String filePath = null;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {//外部存储卡
filePath = Environment.getExternalStorageDirectory().getAbsolutePath();
} else {
Toast.makeText(mContext, "没有SD卡", Toast.LENGTH_SHORT).show();
return;
}
mApkPath = filePath + File.separator + "update.apk";
File file = new File(mApkPath);
if (file.exists()) {
file.delete();
}
Uri fileUri = Uri.parse("file://" + mApkPath);
Uri uri = Uri.parse(mDownloadUrl);
DownloadManager downloadManager = (DownloadManager) mContext
.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(uri);
request.setVisibleInDownloadsUi(true);
request.setTitle(mDownloadTitle);
request.setDestinationUri(fileUri);
mApkId = downloadManager.enqueue(request);
isUpdating = true;
}
这里大概分三步
- 1.SD卡判断和路径设置
- 2.获取DownloadManager并构建下载请求request
- 3.调用enqueue发起异步下载,返回id作为标识,设置更新状态为下载中
UpdateReceiver
UpdateReceiver继承自BroadcastReceiver,监听下载完成,先在AndroidManifest里进行注册receiver
<receiver
android:name=".UpdateReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
<action android:name="android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED"/>
</intent-filter>
</receiver>
然后在onReceive中进行下载完成后的逻辑
if (!DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
return;
}
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(UpdateManager.mApkId);
DownloadManager downloadManager = (DownloadManager) context
.getSystemService(Context.DOWNLOAD_SERVICE);
Cursor cursor = downloadManager.query(query);
if (cursor.moveToFirst()) {
int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
switch (status) {
case DownloadManager.STATUS_PAUSED:
break;
case DownloadManager.STATUS_PENDING:
UpdateManager.isUpdating = true;
break;
case DownloadManager.STATUS_RUNNING:
UpdateManager.isUpdating = true;
break;
case DownloadManager.STATUS_SUCCESSFUL:
UpdateManager.isUpdating = false;
installApk(context);
break;
case DownloadManager.STATUS_FAILED:
UpdateManager.isUpdating = false;
downloadManager.remove(UpdateManager.mApkId);
break;
default:
break;
}
}
通过DownloadManger下载后,系统会发出广播。如果此时还有其它下载任务时,怎么知道完成的是不是我们的apk下载呢?可以通过DownloadManager.Query配合Cursor进行查询
This class may be used to filter download manager queries
在UpdateManager的最后,我们发起下载任务时,返回了一个id,这个id可以设置到Query中, 通过Cursor获取下载状态status,具体用法参考代码或者文档。当下载完成后,设置更新状态为否,同时如果下载成功的话进行自动安装。
private void installApk(Context context) {
if (TextUtils.isEmpty(UpdateManager.mApkPath)) {
return;
}
Uri uri;
Intent intent = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider",
new File(UpdateManager.mApkPath));
} else {
uri = Uri.fromFile(new File(UpdateManager.mApkPath));
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intent);
// 是否关闭app
// android.os.Process.killProcess(android.os.Process.myPid());
}
自动安装的代码比较简单,通过设置下载完成的apk本地uri就可以进行自动安装,不过在7.0之后,对应用外访问的Uri进行处理加密,如果直接传入uri进行安装的话的话,在7.0上的手机会报错,所以需要用到FileProvider
自动安装7.0 适配
关于FileProvider详情参考文档介绍或者Android7.0须知--应用间共享文件(FileProvider)这篇文章,这里只大概说下怎么用。
- 1.在res目录下创建xml文件夹,新建个file_paths文件,文件命名看个人。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path path="" name="updateApk" />
</paths>
</resources>
- 2.在AndroidManifest里注册provider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.xiaoluo.update.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
这里需要注意的是authorities,命名为应用包名+".fileprovider",android:resource是刚刚新建的xml文件
- 3.设置权限FLAG_GRANT_READ_URI_PERMISSION,使用FileProvider.getUriForFile获取uri
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider",
new File(UpdateManager.mApkPath));
}
使用相关
具体可参考源码
- 1.在app的build.gradle中添加依赖
compile 'cn.xiaoluo:update-manager:1.2.0'
- 2.AndroidManifest里进行注册FileProvider,需要把android:authorities中的包名替换成应用包名
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="[包名].fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
- 参数介绍
UpdateManager.getInstance().init(this) // 获取实例并初始化,必要
.compare(UpdateManager.COMPARE_VERSION_NAME) // 通过版本号或版本名比较,默认版本号
.downloadUrl("http://aaa.apk") // 下载地址,必要
.downloadTitle("我在下载xxxb了") // 下载标题
.lastestVerName("1.0") // 最新版本名
.lastestVerCode(0) // 最新版本号
.minVerName("1.0") // 最低版本名
.minVerCode(1) // 最低版本号
.isForce(true) // 是否强制更新,true无视版本直接更新
.update() // 开始更新
// 设置版本对比回调
.setListener(new UpdateManager.UpdateListener() {
@Override
public void onCheckResult(String result) {
Toast.makeText(mContext, result, Toast.LENGTH_SHORT).show();
}
});
- 简单使用
UpdateManager.getInstance().init(this)
.downloadUrl("http://aaa.apk")
.lastestVerCode(2)
.update();
- 版本对比:
版本名:采用版本管理格式(如1.0.0),依次对比各个数字,越往前决定权越高,如2.0.1 > 1.9.9
当前前面各数字均一致时,以更长的为高版本,如1.2.2.1 > 1.2.2
版本号:数值高的为高版本