1.背景
在工作上遇到了一个关于在Android7.0下载更新安装包无法安装的兼容性问题,特记录下来,作为典型兼容问题的积累和专业知识的积累,以及提醒自己在后续的测试工作中要遇到系统版本更新时,要做到系统性地分析版本特性,然后针对自身项目发起平台/系统升级的补充测试,提高测试覆盖率,保障产品质量。
2.问题描述
测试组里某个项目产品APP进行更新升级功能测试时,出现了一个问题,就是发现在自动更新功能的时候,下载好了apk的文件后不能自动跳到安装界面,导致无法安装相应的新版本,发现这个问题只会发生在Android 7.0版本的设备上,在较低版本的设备上则无这个问题。
3.问题引入原因分析
3.1 先来了解什么是APP更新升级功能
app在线更新是一个比较常见需求,新版本发布时,用户进入我们的app,就会弹出更新提示框,第一时间更新新版本app。在线更新分为以下几个步骤:
1, 通过接口获取线上版本号,versionCode
2, 比较线上的versionCode 和本地的versionCode,弹出更新窗口
3, 下载APK文件(文件下载)
4,安装APK
在线更新就上面几个步骤,前2步比较简单,重要的就是后2个步骤,而由于Android 各个版本对权限和隐私的收归和保护,因此,可能会出现各种的适配问题
3.2 理解安装APK的实现原理
上一节讲到由于Android 各个版本对权限和隐私的收归和保护,因此,下载和安装apk时可能会出现各种的适配问题,由于此bug是安装apk时出现的问题,所以我们就来重点分析一下安装apk的实现原理。
安装APK步骤
一般安装apk之前是先下载apk,一般最简单的方式是用 DownloadManager 来下载apk。
DownloadManager 是SDK 自带的,大概流程如下:
(1)创建一个Request,进行简单的配置(下载地址,和文件保存地址等)
(2)下载完成后,系统会发送一个下载完成的广播,我们需要监听广播。
(3)监听到下载完成的广播后,根据id查找下载的apk文件
(4)在代码中执行apk安装。
DownloadManager在下载完成之后,会发送一个下载完成的广播DownloadManager.ACTION_DOWNLOAD_COMPLETE,我们只需要监听这个广播,收到广播后, 获取apk文件安装。
定义一个广播DownloadReceiver:
class DownloadReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
// 安装APK
long completeDownLoadId = intent.getLongExtra(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 = mDownloadManager.getUriForDownloadedFile(completeDownLoadId);
}
intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intentInstall);
}
}
在下载之前注册广播
// 注册广播,监听APK是否下载完成
weakReference.get().registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
通过上面的几个步骤,基本上就完成app在线更新功能,在Android 6.0以下可以正常运行。
3.3 Android7.0系统 文件访问权限特性
每个Android版本的发布,对于安全性问题的要求越来越高,也为Android程序员增加了额外的工作量。
Android7.0引入私有目录被限制访问和StrictMode API 。
私有目录被限制访问是指在Android7.0中为了提高应用的安全性,在7.0上应用私有目录将被限制访问,这与iOS的沙盒机制类似。
StrictMode API是指禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,则会报出异常,也就是说不能访问你应用私有的文件夹了
带来的影响:这项权限的变更将意味着你无法通过File API访问手机存储上的数据了,基于File API的一些文件浏览器等也将受到很大的影响,例如文件下载安装、上传图片功能、系统相机拍照,或裁切照片功能等。
3.4 得出BUG引入原因
综上所述,我们理解了更新升级的功能和功能实现原理,以及通过对Android系统文件访问权限的特性的详细分析,得出在Android 7.0上,对文件的访问权限作出了修改,从代码中可以看出,Uri.fromFile导致我们在7.0上出现了问题,它其实就是生成一个file://URL。这就是为什么在下载完成后,无法进行自动安装,因为一旦我们通过这种办法打开系统安装器,就认为file:// URI类型的 Intent 离开我的应用,这样程序就会发生异常;
所以在Android7.0上,不能在使用file://格式的Uri 访问文件 ,Android 7.0提供 FileProvider,应该使用这个来获取apk地址,然后安装apk。
4.解决方案
解决方案那就是允许共享你私有目录下的一个文件夹,共享出去让大家访问,这样就可以访问你下载的apk来安装了,将使用FileProvider,它的步骤是:
- 第一步:
在AndroidManifest.xml中注册provider,provider可以向应用外提供数据。
<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>
- 第二步:
在res/xml/file_paths.xml创建文件。 内容为:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path path="" name="download"/>
</paths>
</resources>
这个要说明一下
<files-path/>代表的根目录: [Context.getFilesDir()](https://developer.android.com/reference/android/content/Context.html?hl=zh-tw#getFilesDir())
<external-path/>代表的根目录: [Environment.getExternalStorageDirectory()](https://developer.android.com/reference/android/os/Environment.html?hl=zh-tw#getExternalStorageDirectory())
<cache-path/>代表的根目录: [getCacheDir()](https://developer.android.com/reference/android/content/Context.html?hl=zh-tw#getCacheDir())
这样就把这个目录给共享出去了
- 第三步:通过FileProvider获取URI进行安装成功
if(Build.VERSION.SDK_INT>=24) {//判读版本是否在7.0以上
Uri apkUri = FileProvider.getUriForFile(this, "你的包名.fileprovider", apkFile);//在AndroidManifest中的android:authorities值
Intent install = new Intent(Intent.ACTION_VIEW);
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
install.setDataAndType(apkUri, "application/vnd.android.package-archive");
startActivity(install);
} else{
Intent install = new Intent(Intent.ACTION_VIEW);
install.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(install);
}
5.总结
将功能代码实现和系统平台特征分析结合起来,运用到测试实践中,还是蛮重要的,通过看别人的代码了解功能的实现方式,进而思考这种实现方式在各系统版本中可能会存在的适配问题,避免掉一些由于对系统对功能实现不了解而忽略的场景,使得测试覆盖率更高,更精准。
另外,一个困扰自己超过2个小时的问题有必要整理下来,这篇小文章不过用了30分钟整理,但是积累多了也是一份宝贵的财富。