Android:8.0中未知来源安装权限变更
哎,Android 9.0 都开始推了,但我却在 8.0 的特性中栽了跟头!
这就是不好好学习,不及时适配的后果!!
一、问题现象
在测试APK升级逻辑时,偶然发现在8.0系统的手机中,APK下载完就没有然后了,没有弹出安装界面,不执行安装逻辑。但是在8.0之前的版本中可以正常下载,正常弹起安装界面。
二、问题分析
查阅相关资料发现,Android8.0中对于APK的安装做了如下调整:
- 将 设置--安全 中的 允许安装未知来源应用 取消了(由于国内手机系统的高度定制,该选择项的位置有差异)
- 在安装 APK 文件时新增 未知来源安装权限,即
android.permission.REQUEST_INSTALL_PACKAGES
也就是说,在Android 8.0(即Android O) 之前,设置 中的 允许安装未知来源 是针对所有APP的,只要开启了,那么所有的未知来源APP都可以安装。但是,8.0之后,将这个权限挪到了每一个APP内部,这样提高了手机的安全性,降低了流氓软件的安装概率。
参考资料: Making it safer to get apps on Android O
三、解决方案
(1)、步骤1
按照上面参考资料中的说明,现在 AndroidMainfest.xml 清单文件中增加如下权限
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
(2)、步骤2
在上述参考资料中,有下面这么一段话:
You can choose to pre-emptively direct your users to the Install unknown apps permission screen using the ACTIONMANAGEUNKNOWNAPPSOURCES Intent action. You can also query the state of this permission using the PackageManager canRequestPackageInstalls() API.
上面这段话意思是说,
- 我们通过 ACTIONMANAGEUNKNOWNAPPSOURCES 这个Action可以跳转到 未知来源安装设置界面,引导用户去开启这个选项。
- 我们可以通过PackageManager中的canRequestPackageInstalls()来检测是否已经开启了未知来源安装权限。true 表示获取了权限,false 表示没有获取权限。为fasle 时,安装过程会被中断,无法跳转到安装界面。
所以,我们在下载完APK之后,可以按照下面的流程来处理代码:
具体示例代码如下:
- 下载逻辑省略,此处只列出 未知来源权限和安装 的处理逻辑
- 下面的逻辑实在 WelcomeActivity中实现的,所以,可以直接使用 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) {
//CnPeng 2018/8/2 下午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();
}
}
}
/**
* 作者:CnPeng
* 时间:2018/8/2 下午6:06
* 功用:弹窗请安装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();
}
});
}
/**
* 作者:CnPeng
* 时间:2018/8/2 下午5:50
* 功用:未知来源权限弹窗
* 说明: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();
}
});
}
四、总结
(1)、个人总结
在关注新版本特性时,不能只关注新控件,其他系统级的变更必须高度重视。这次的8.0安装权限变更就是一个教训啊!!
(2)、参考资料附录
Making it safer to get apps on Android O
- 通过一组RESTful API暴露CQRS系统功能
- 通过使用结构化数据 JSON-LD,我为网站带来了更多的流量
- 使用 OWIN Self-Host ASP.NET Web API 2
- c#开源消息队列中间件EQueue 教程
- Serverless 框架 OpenWhisk 开发指南:使用 Node.js 编写 hello, world
- GOTO Berlin: Web API设计原则
- 使用 ServiceStack 构建跨平台 Web 服务
- 使用 OpenWhisk 自建 Serverless 服务
- 如何在 8 小时内开发上线一个在线表单系统
- 让Response.Redirect页面重定向更有效率
- 使用 adr 轻松创建 “程序员友好” 的轻量级文档
- 在Linux和Windows平台上操作MemoryMappedFile(简称MMF)
- android来电归属地提醒
- Jexus 支持PHP的三种方式
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- CNN一定需要池化层吗?
- RabbitMQ入门Demo,基于springboot
- 收藏|Pandas缺失值处理看这一篇就够了!
- Spring Boot项目页面报错 OTS parsing error: Failed to convert WOFF 2.0
- Spring Boot开启JSP页面热部署
- Springboot thymeleaf热部署
- Java初始化List的6种方式
- Java遍历Map对象的四种方式
- 【SpringBoot源码解析】第三章:SpringBoot通过打成war包的方式是如何启动的
- 让你编码嗨到停不下来的8个VSCode插件
- 【SpringBoot源码解析】第四章:SpringBoot是如何自动装配SpringMvc的
- 【SpringBoot源码解析】第二章:SpringBoot是如何通过内置Tomcat启动的
- 技术译文 | How Can ScaleFlux Handle MySQL Workload?
- 技术译文 | MySQL 8 需要多大的 innodb_buffer_pool_instances 值(上)
- 前端登录,这一篇就够了