我们希望应用的下载更新可以不受UI周期的约束,这里下载就涉及到Google提供的大文件下载管理类DownloadManager,下载完成后通过BroadcastReceive接收下载完成的消息开启应用安装。下面正式开启步骤解析
本博客Demo地址:https://download.csdn.net/download/g_ying_jie/10697856
第一步,传入apk的下载地址,利用DownloadManager下载安装包
protected static void downloadApk(Context context, String apkUrl, String apkName) {
//获取DownloadManager对象
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
//指定APK缓存路径和应用名称,可在SD卡/Android/data/包名/file/Download文件夹中查看
request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, apkName.concat(".apk"));
//设置网络下载环境为wifi
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
//设置显示通知栏,下载完成后通知栏自动消失
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
//设置通知栏标题
request.setTitle("we_chat");
request.setDescription("APK下载中...");
request.setAllowedOverRoaming(false);
//获得唯一下载id
DOWNLOAD_ID = downloadManager.enqueue(request);
}
DOWNLOAD_ID是下载的唯一标识,后期可以用来查询下载的文件信息。
第二步,新建BroadcastReceiver接收下载完成消息,并回调状态
package com.example.gu.download;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.support.annotation.RequiresApi;
import android.support.v4.content.FileProvider;
import android.text.TextUtils;
import java.io.File;
import java.util.Objects;
public class UpdateBroadcastReceiver extends BroadcastReceiver {
private DownloadCompleteCallBack callBack;
public UpdateBroadcastReceiver(DownloadCompleteCallBack callBack) {
this.callBack = callBack;
}
public void onReceive(Context context, Intent intent) {
//下载完成
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if (id == UpdateUtil.DOWNLOAD_ID) {
callBack.canInstall(id);
}
}
public interface DownloadCompleteCallBack {
void canInstall(long id);
}
}
第三步,在MainActivity注册广播,注入DownloadCompleteCallBack的回调
receiver = new UpdateBroadcastReceiver(this);
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
registerReceiver(receiver, filter);
不要忘记注销
@Override
protected void onDestroy() {
super.onDestroy();
if (receiver != null)
unregisterReceiver(receiver);
}
第四步,在回调的canInstall中执行安装流程
protected static void installApp(Context mContext, long id) {
File apkFile = queryApkPath(mContext, id);
if (apkFile != null) {
try {
//8.0跳转设置允许安装未知应用
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = mContext.getPackageManager().canRequestPackageInstalls();
if (!hasInstallPermission) {
startInstallPermissionSettingActivity(mContext);
return;
}
}
Intent intent = new Intent(Intent.ACTION_VIEW);
//兼容7.0之后禁用在应用外部公开file://URI,以FileProvider替换
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//给目标应用一个临时授权
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//注意此处的authority必须与manifest的provider保持一致
intent.setDataAndType(FileProvider.getUriForFile(mContext, mContext.getPackageName().concat(".fileprovider"), apkFile), "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
}
//验证是否有APP可以接受此Intent,防止FC
if (mContext.getPackageManager().queryIntentActivities(intent, 0).size() > 0) {
mContext.startActivity(intent);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
其中queryApkPath方法调用下载返回的ID查询apk文件信息,代码如下
private static File queryApkPath(Context context, long id) {
File apkFile = null;
DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(id);
Cursor cur = manager.query(query);
if (cur != null) {
if (cur.moveToFirst()) {
// 获取文件下载路径
String filePath = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
if (!TextUtils.isEmpty(filePath)) {
apkFile = new File(Objects.requireNonNull(Uri.parse(filePath).getPath()));
}
}
cur.close();
}
return apkFile;
}
startInstallPermissionSettingActivity方法用于8.0之后跳转允许安装未知应用的设置页面,代码如下
/**
* 跳转到设置-允许安装未知来源-页面
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private static void startInstallPermissionSettingActivity(Context mContext) {
Uri packageURI = Uri.parse("package:".concat(mContext.getPackageName()));
//注意这个是8.0新API
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI);
((Activity) mContext).startActivityForResult(intent, INSTALL_REQUESTCODE);
}
第五步,在onActivityResult方法中监听8.0未知应用授权情况,授权成功再次发起安装流程
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == UpdateUtil.INSTALL_REQUESTCODE && resultCode == RESULT_OK) {
UpdateUtil.installApp(MainActivity.this, DOWNLOAD_ID);
} else {
Toast.makeText(this, "授权失败,应用未能安装", Toast.LENGTH_LONG).show();
}
}
敲黑板划重点,整体的流程都已经走完了,细心的读者会发现在7.0之后引入了FileProvider来防止在应用外部公开file://URI,
那么这个该怎么配置各种属性呢?接着往下看
FileProvider使用第一步,在res下新建xml文件夹,在其下新建一个file_paths.xml文件,如下
path就是FileProvider共享的文件夹目录,即/storage/emulated/0/Android/data/包名/files/Download文件夹被共享
其他属性汇总:更多有关FileProvider属性可前往FileProvider详解
files-path ==> /data/data/包名/files
cache-path ==> /data/data/com.jph.simple/cache
external-path ==> /storage/emulated/0
external-cache-path ==> /storage/emulated/0/Android/data/包名/cache
等同于如下路径:
代表设备的根目录new File("/");
代表context.getFilesDir()
代表context.getCacheDir()
代表Environment.getExternalStorageDirectory()
代表context.getExternalFilesDirs()
代表getExternalCacheDirs()
FileProvider使用第二步,在manifest中申明provider
注解:
name的值一般都固定为android.support.v4.content.FileProvider。如果开发者继承了FileProvider,则可以写上其绝对路径。
authorities字段的值用来表明使用的使用者,在FileProvider的函数getUriForFile需要传入该参数,通用写法是包名+fileprovider。
exported 的值为false,表示该FileProvider只能本应用使用,不是public的。
grantUriPermissions 的值为true,表示允许赋予临时权限。