Androidapp在线更新那点事⼉(适配Android6.0、7.0、8.0)
app在线更新是⼀个⽐较常见需求,新版本发布时,⽤户进⼊我们的app,就会弹出更新提⽰框,第⼀时间更新新版本app。在线更新分为以下⼏个步骤:
1, 通过接⼝获取线上版本号,versionCode
2, ⽐较线上的versionCode 和本地的versionCode,弹出更新窗⼝
3, 下载APK⽂件(⽂件下载)
4,安装APK
在线更新就上⾯⼏个步骤,前2步⽐较简单,重要的就是后2个步骤,⽽由于Android 各个版本对权限和隐私的收归和保护,因此,会出现各种的适配问题,因此本⽂就总结⼀下app 在线更新⽅法和遇到的⼀些适配问题。
apk下载其实就是⽂件下载,⽽⽂件下载有很多⽅式:
1,很多三⽅框架都有⽂件上传下载功能,可以借助三⽅框架(⽐如Volley,OkHttp)
2,也可以开启⼀个线程去下载,(可以⽤IntentService)
3,最简单的⼀种⽅式:Android SDK 其实给我们提供了下载类DownloadManager,只需要简单的配置项设置,就能轻松实现下载功能。
本⽂就⽤第三种⽅式,⽤DownloadManager来下载apk。
1. 使⽤DownloadManager下载apk
DownloadManager 是SDK ⾃带的,⼤概流程如下:
(1)创建⼀个Request,进⾏简单的配置(下载地址,和⽂件保存地址等)
(2)下载完成后,系统会发送⼀个下载完成的⼴播,我们需要监听⼴播。
(3)监听到下载完成的⼴播后,根据id查下载的apk⽂件
(4)在代码中执⾏apk安装。
public void downloadApk(String apkUrl, String title, String desc) {
// fix bug : 装不了新版本,在下载之前应该删除已有⽂件
File apkFile = new ().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "test.apk");
if (apkFile != null && ists()) {
apkFile.delete();
}
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
//设置title
request.setTitle(title);
// 设置描述
request.setDescription(desc);
// 完成后显⽰通知栏
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.(), Environment.DIRECTORY_DOWNLOADS, "test.apk");
//在⼿机SD卡上创建⼀个download⽂件夹
// ExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).mkdir() ;
//指定下载到SD卡的/download/my/⽬录下
// request.setDestinationInExternalPublicDir("/codoon/","test.apk");
request.setMimeType("application/vnd.android.package-archive");
//记住reqId
mReqId = queue(request);
}
如上代码所⽰,⾸先构建⼀个Request,设置下载地址,标题、描述、apk存放⽬录等,最后,调⽤queue(request)开始下载。
注意:这⾥我们需要记住这个mReqId,因为下载完成之后,我们需要根据这个ID 去查apk⽂件,然后安装apk.
2.更新下载进度
下载⽂件,我们⼀般需要知道下载的进度,在界⾯给⽤户⼀个友好的提⽰,app 更新也是⼀样,我们需要在界⾯上显⽰当前下载进度和总进度,让⽤户知道⼤概会等待多久。那么如果获取下载进度呢?
在下载之前,我们需要在Activity 中注册⼀个Observer,就是⼀个观察者,当下载进度变化的时候,就会通知观察者,从⽽更新进度。步骤如下:
1, ⾸先我们先定义⼀个观察者DownloadChangeObserver来观察下载进度
2,在DownloadChangeObserver 中更新UI进度,给⽤户提⽰
3,下载之前,在Activity 中注册Observer
具体代码如下:
DownloadChangeObserver.class:
class DownloadChangeObserver extends ContentObserver {
/**
* Creates a content observer.
*
* @param handler The handler to run {@link #onChange} on, or null if none.
*/
public DownloadChangeObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
updateView();
}
}
在updateView()⽅法中,查询下载进度。
private void updateView() {
int[] bytesAndStatus = new int[]{0, 0, 0};
DownloadManager.Query query = new DownloadManager.Query().setFilterById(mReqId);
Cursor c = null;
try {
c = mDownloadManager.query(query);
if (c != null && c.moveToFirst()) {
//已经下载的字节数
bytesAndStatus[0] = c.ColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
//总需下载的字节数
bytesAndStatus[1] = c.ColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
//状态所在的列索引
bytesAndStatus[2] = c.ColumnIndex(DownloadManager.COLUMN_STATUS));
}
} finally {
if (c != null) {
c.close();
}
}
if (mUpdateListener != null) {
mUpdateListener.update(bytesAndStatus[0], bytesAndStatus[1]);
}
Log.i(TAG, "下载进度:" + bytesAndStatus[0] + "/" + bytesAndStatus[1] + "");
}
根据前⾯我们记录的ID去查询进度,代码中已经注释了,不再多讲。
要想获取到进度,在下载之前,还得先注册DownloadChangeObserver,代码如下:
<().getContentResolver().registerContentObserver(Uri.parse("content://downloads/my_downloads"), true,
mDownLoadChangeObserver);
3. 获取下载结果
DownloadManager在下载完成之后,会发送⼀个下载完成的⼴播DownloadManager.ACTION_DOWNLOAD_COMPLETE,我们只需要监听这个⼴播,收到⼴播后, 获取apk⽂件安装。
定义⼀个⼴播DownloadReceiver。
class DownloadReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
/
/  安装APK
long completeDownLoadId = LongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
Logger.e(TAG, "收到⼴播");
Uri uri;
Intent intentInstall = new Intent();
intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intentInstall.setAction(Intent.ACTION_VIEW);
if (completeDownLoadId == mReqId) {
uri = UriForDownloadedFile(completeDownLoadId);
}
intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intentInstall);
}
}
在下载之前注册⼴播
// 注册⼴播,监听APK是否下载完成
<().registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
通过上⾯的⼏个步骤,基本上就完成app在线更新功能,在Android 6.0以下可以正常运⾏。但是别忙,本⽂还没有结束,Android每⼀个版本都有⼀些改动,导致我们需要适配不同的版本,不然的话,就会出问题,结下来就看⼀下Android 6.0,7.0,8.0 的相关适配。
通过前⾯讲的⼏个步骤,app 在线更新在6.0以下已经可以正常运⾏,在Android6.0上,安装的时候会报出以下错误:
Caused by:
t.ActivityNotFoundException:No Activity found to handle Intent { act=android.intent.action.VIEW typ=application/vnd.android.package-archive flg=0x10000000 }
为什么会报上⾯的错误,经过debug发现,在Android6.0以下和Android6.0上,通过DownloadManager 获取到的Uri不⼀样。
区别如下:(1)Android 6.0,getUriForDownloadedFile得到值为:
(2) Android6.0以下,getUriForDownloadedFile得到的值
为:file:///storage/emulated/0/Android/data/packgeName/files/Download/xxx.apk
可以看到,Android6.0得到的apk地址为:content:// 开头的⼀个地址,安装的时候就会报上⾯的错误。怎么解决呢?经过查资料到了解决办法:
//通过downLoadId查询下载的apk,解决6.0以后安装的问题
public static File queryDownloadedApk(Context context, long downloadId) {
File targetApkFile = null;
DownloadManager downloader = (DownloadManager) SystemService(Context.DOWNLOAD_SERVICE);
if (downloadId != -1) {
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(downloadId);
query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
Cursor cur = downloader.query(query);
if (cur != null) {
if (veToFirst()) {
String uriString = ColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
if (!TextUtils.isEmpty(uriString)) {
targetApkFile = new File(Uri.parse(uriString).getPath());
}
}
cur.close();
}
}
return targetApkFile;
}
代码如上所⽰,不通过getUriForDownloadedFile去获取Uri,通过DownloadManager.COLUMN_LOCAL_URI这个字段去获取apk地址。
适配Android 6.0后,安装apk 的代码如下:
/**
* @param context
* @param intent
*/
private void installApk(Context context, Intent intent) {
long completeDownLoadId = LongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
Logger.e(TAG, "收到⼴播");
Uri uri;
Intent intentInstall = new Intent();
intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intentInstall.setAction(Intent.ACTION_VIEW);
if (completeDownLoadId == mReqId) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 6.0以下
uri = UriForDownloadedFile(completeDownLoadId);
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // 6.0 - 7.0
File apkFile = queryDownloadedApk(context, completeDownLoadId);
uri = Uri.fromFile(apkFile);
}
// 安装应⽤
Logger.e("zhouwei", "下载完成了");
intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intentInstall);
}
}
刚适配完6.0,在7.0以上的机⼦上⼜出问题了,为什么呢?因为在Android 7.0上,对⽂件的访问权限作出了修改,不能在使⽤file://格式的Uri 访问⽂件,Android 7.0提供FileProvider,应该使⽤这个来获取apk地址,然后安装apk。如下进⾏简单的适配:
(1)在res ⽬录下,新建⼀个xml⽂件夹,在xml下⾯创建⼀个⽂件provider_paths⽂件:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external"
path="" />
<external-files-path
name="Download"
path="" />
</paths>
(2) 在l清单⽂件中申明Provider:
<!-- Android 7.0 照⽚、APK下载保存路径-->
<provider
android:name="android.t.FileProvider"
android:authorities="packgeName.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
(3) Android 7.0上的⽂件地址获取:
uri = UriForFile(context,
"packageNam.fileProvider",
new ExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "xxx.apk"));
好了,就这样7.0适配⼯作就完成了,适配后的安装代码如下:
/**
* @param context
* @param intent
*/
private void installApk(Context context, Intent intent) {
long completeDownLoadId = LongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
Logger.e(TAG, "收到⼴播");
null官方更新地址Uri uri;
Intent intentInstall = new Intent();
intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intentInstall.setAction(Intent.ACTION_VIEW);
if (completeDownLoadId == mReqId) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 6.0以下
uri = UriForDownloadedFile(completeDownLoadId);
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // 6.0 - 7.0
File apkFile = queryDownloadedApk(context, completeDownLoadId);
uri = Uri.fromFile(apkFile);
} else { // Android 7.0 以上
uri = UriForFile(context,
"packgeName.fileProvider",
new ExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "xxx.apk"));
intentInstall.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
// 安装应⽤
Logger.e("zhouwei", "下载完成了");
intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intentInstall);
}
}
注意:把上⾯的packageNam 换成你⾃⼰的包名,把xxx.apk 换成你⾃⼰的apk的名字。
关于更多FileProvider的东西,这⼉就不展开讲了,想要了解的可以看⼀下鸿洋的⽂章:
,讲的很清楚。
好特么累,继续适配Android 8.0, 由于没有Android 8.0的⼿机,⼀直没有注意,前些天⼀个华为⽤户反馈在线更新不了新版本,具体表现就是:apk下载完成,⼀闪⽽过,没有跳转到apk安装界⾯。经过排查,确定了是Android 8.0权限问题。
Android8.0以上,未知来源的应⽤是不可以通过代码来执⾏安装的(在sd卡中到apk,⼿动安装是可
以的),未知应⽤安装权限的开关被除掉,取⽽代之的是未知来源应⽤的管理列表,需要列表⾥⾯开启你的应⽤的未知来源的安装权限。Google这么做是为了防⽌⼀开始正经的应⽤后来开始通过升级来做⼀些不合法的事情,侵犯⽤户权益。
知道问题了,我们就适配吧:
(1)在清单⽂件中申明权限:REQUEST_INSTALL_PACKAGES
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。