1.问题的现象
在我们测试APK升级的时候,会遇到8.0在下载完成 以后就没有然后了。没有弹起安装页面,不执行安装逻辑。但是在8.0之前的版本,可以正常下载可以正常弹起安装页面。
2.问题的分析
通过查询资料得到,Android8.0以后增加一个未知来源权限。
将设置---安全中的允许安装未知来源应用取消了(由于国内的手机系统的高度定制,该选择项的位置有差异)
在安装APK文件时新增 ,未知来源安装权限 ,
android.permission.REQUEST_INSTALL_PACKAGES
也就是说,在安卓8.0以后(Android o)之前,设置中的允许安装来源是针对所有App的,只要开启了,那么所有位置来源的App都会安装,但是在8.0之后,将这个权限挪到每一个App的内部,这样大大的提高了手机的安全性,降低了流氓软件的安装概率。
参考资料: Making it safer to get apps on Android O
3.解决办法
(1)、步骤一
在AndroidMainfest.xml清单文件中增加如下权限
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
(2)、步骤二
- 我们通过ACTION_MANAGE_UNKNOWN_APP_SOURCES
这个Action可以跳转到未知来源安装设置界面,引导用户去开启这个选项。 - 我们可以通过PackageManager中canRequestPackageInstalls()来检测是否已经开启了未知来源安装权限。true表示获取了权限,false表示没有获取权限。为false时,安装过程会被中断,无法跳转到安装页面。
所以,我们在下载完APK之后,可以按照下面的流程来处理流程图
以我项目中的为例子下载的代码
//下载的代码
private void downFile(final String versionUrl) {
//创建一个下载提醒框
ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setCancelable(false);
progressDialog.setTitle("下载中。。。。");
progressDialog.setProgress(0);
progressDialog.show();
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient build = new OkHttpClient.Builder().build();
Request request = new Request.Builder().url(versionUrl).build();
build.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
int length = (int) response.body().contentLength();
InputStream inputStream = response.body().byteStream();
progressDialog.setMax(length);
FileOutputStream fos = null;
if (inputStream != null) {
File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "jiuxing.apk");
fos = new FileOutputStream(file);
byte[] buf = new byte[1024];
int ch;
int process = 0;
while ((ch = inputStream.read(buf)) != -1) {
fos.write(buf, 0, ch);
process += ch;
progressDialog.setProgress(process); // 实时更新进度了
}
if (fos != null) {
fos.flush();
fos.close();
}
//关闭dialog防止泄露
progressDialog.dismiss();
// 下载完成后安装 代码省略
...............
}
}
});
}
}).start();
}
- 下面的逻辑可以在我们的主页中实现 可以直接使用
startActivityForResult
并在onActivityResult
中解析数据
/**
* 打开安装包
*/
private void openAPKFile() {
String mimeDefault = "application/vnd.android.package-archive";
File apkFile = null;
if (!TextUtils.isEmpty(mApkUri)) {
//mApkUri是apk下载完成后在本地的存储路径
apkFile = new File(Uri.parse(mApkUri).getPath());
}
if (apkFile == null) {
return;
}
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//兼容7.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//这里牵涉到7.0系统中URI读取的变更
Uri contentUri = FileProvider.getUriForFile(mActivity, getPackageName() + ".fileprovider", apkFile);
intent.setDataAndType(contentUri, mimeDefault);
//兼容8.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = getPackageManager().canRequestPackageInstalls();
if (!hasInstallPermission) {
startInstallPermissionSettingActivity();
return;
}
}
} else {
intent.setDataAndType(Uri.fromFile(apkFile), mimeDefault);
}
if (getPackageManager().queryIntentActivities(intent, 0).size() > 0) {
//如果APK安装界面存在,携带请求码跳转。使用forResult是为了处理用户 取消 安装的事件。外面这层判断理论上来说可以不要,但是由于国内的定制,这个加上还是比较保险的
startActivityForResult(intent, 2);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
/**
* 跳转到设置-允许安装未知来源-页面
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private void startInstallPermissionSettingActivity() {
//后面跟上包名,可以直接跳转到对应APP的未知来源权限设置界面。使用startActivityForResult 是为了在关闭设置界面之后,获取用户的操作结果,然后根据结果做其他处理
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, 1);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
if (requestCode == 1) {
openAPKFile();
}
} else {
if (requestCode == 1) {
//下午4:31 8.0手机位置来源安装权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = getPackageManager().canRequestPackageInstalls();
if (!hasInstallPermission) {
LogUtils.e(TAG, "没有赋予 未知来源安装权限");
showUnKnowResourceDialog();
}
}
} else if (requestCode == 2) {
// CnPeng 2018/8/2 下午4:31 在安装页面中退出安装了
LogUtils.e(TAG, "从安装页面回到欢迎页面--拒绝安装");
showApkInstallDialog();
}
}
}
/**
* 功用:弹窗请安装APP的弹窗
* 说明:8.0手机升级APK时获取了未知来源权限,并跳转到APK界面后,用户可能会选择取消安装,所以,再给一个弹窗
*/
private void showApkInstallDialog() {
final CustomAlertDialog installDialog = new CustomAlertDialog(mActivity);
installDialog.setCancelable(false);
DialogInstallApkBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_install_apk, null, false);
installDialog.setView(binding.getRoot());
installDialog.show();
binding.ivIKnowBt2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//再次回到安装界面
openAPKFile();
}
});
binding.tvInstallNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
installDialog.dismiss();
//CnPeng 2018/8/2 下午5:28 使用自定义方法关闭全部activity
ActivitiesCollector.finishAll();
}
});
}
/**
* 说明:8.0系统中升级APK时,如果跳转到了 未知来源权限设置界面,并且用户没用允许该权限,会弹出此窗口
**/
private void showUnKnowResourceDialog() {
final CustomAlertDialog alertDialog = new CustomAlertDialog(mActivity);
alertDialog.setCancelable(false);
DialogUnknowResourceBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_unknow_resource, null, false);
alertDialog.setView(binding.getRoot());
alertDialog.show();
binding.ivIKnowBt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//兼容8.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = getPackageManager().canRequestPackageInstalls();
if (!hasInstallPermission) {
startInstallPermissionSettingActivity();
}
}
alertDialog.dismiss();
}
});
}
4.个人总结
在关注新版本特性时,不能只关注新控件,其他系统级的变更必须高度重视。这次的8.0安装权限变更就是一个教训啊!