概要
一般地, Android App 都会被要求在App内进行软件更新提示, 让用户下载apk文件, 然后更新安装新版本, 一般过程如下:
- 检测是否有新版本
- 下载新版本app apk文件
- 安装新的apk
通常我们将apk文件存放在外部存储上.然后将 文件路径传递给系统, 进行apk的安装.
文件路径传递过程
安装代码如下:
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(newApk), "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
通常情况下这样没问题, 但是在Android7.0 系统上会出现异常 FileUriExposedException
, 这是因为在7.0 系统上发生的权限变更:
系统权限更改
为了提高私有文件的安全性,面向 Android 7.0 或更高版本的应用私有目录被限制访问 (0700)。此设置可防止私有文件的元数据泄漏,如它们的大小或存在性。此权限更改有多重副作用:
私有文件的文件权限不应再由所有者放宽,为使用 MODE_WORLD_READABLE 和/或 MODE_WORLD_WRITEABLE 而进行的此类尝试将触发 SecurityException。
注:迄今为止,这种限制尚不能完全执行。应用仍可能使用原生 API 或 File API 来修改它们的私有目录权限。但是,我们强烈反对放宽私有目录的权限。
传递软件包网域外的 file:// URI 可能给接收器留下无法访问的路径。因此,尝试传递 file:// URI 会触发 FileUriExposedException。分享私有文件内容的推荐方法是使用 FileProvider。
DownloadManager 不再按文件名分享私人存储的文件。旧版应用在访问 COLUMN_LOCAL_FILENAME 时可能出现无法访问的路径。面向 Android 7.0 或更高版本的应用在尝试访问 COLUMN_LOCAL_FILENAME 时会触发 SecurityException。通过使用 DownloadManager.Request.setDestinationInExternalFilesDir() 或 DownloadManager.Request.setDestinationInExternalPublicDir() 将下载位置设置为公共位置的旧版应用仍可以访问 COLUMN_LOCAL_FILENAME 中的路径,但是我们强烈反对使用这种方法。对于由 DownloadManager 公开的文件,首选的访问方式是使用ContentResolver.openFileDescriptor()。
上述说明在7.0 系统上使用新的 API 来进行文件传递.
Android 7.0 的文件传递
官方推荐使用 FileProvider
, 那我们看看如何使用
1. 在 Manifest 文件添加 provider, 如下:
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
...
</application>
</manifest>
添加 provider
2. 设置文件路径
在 res/
目录下创建 xml
文件夹, 然后穿件文件 provider_paths.xml
, 其内容格式如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<files-path name="files" path="" />
<cache-path name="cache" path="" />
<external-path name="external" path="" />
<external-files-path name="external_file_path" path="" />
<external-cache-path name="external_cache_path" path="" />
<external-media-path name="external_media_path" path="" />
</paths>
</resources>
指定的路径有6中,其中:
代表你的app内部存储区域 files/ 目录下的文件, 等同于 Context.getFilesDir()
.
代表你的app内部存储区域 files/ 目录下的文件, 等同于 Context.getCacheDir()
.
代表外部存储根目录下的文件路径, 等同于 Environment.getExternalStorageDirectory()
.
代表你的app外部存储区域根木目录下的路径, 等同于 Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
代表你的app外部存储区域缓存目录下的路径, 等同于 Context.getExternalCacheDir()
代表你的app外部存储区域媒体目录下的路径, 等同于 Context.getExternalMediaDirs()
根据自己的需要,设置路径.
3. 代码中使用FileProvider
Uri contentUri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", newApk);
这样可以将 Uri 进行传递.
安装过程
一般我们直接将apk路径传递给系统进行安装, 如上面的代码:
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(newApk), "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
但是在 Android8.0 又有权限变更, 需要单独的安装权限:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
所以当在 8.0及以上的系统安装apk是还需添加此权限到 Manifest 文件, 并做判断.
private void checkInstallApkPermission(String filePath) {
L.i(TAG, "checkInstallApkPermission:" + filePath);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (getPackageManager().canRequestPackageInstalls()) {
L.i(TAG, "can request package");
launchUpgrade(new File(filePath));
} else {
L.i(TAG, "can not request package");
launchUpgrade(new File(filePath));
}
} else {
launchUpgrade(new File(filePath));
}
}
可能会有人疑惑, 这里为什么if else 都是直接 启动升级安装?
因为目前国内的定制系统会已经添加授权弹窗, 如果你采用网络所说的直接跳到 Setting界面, 让用户手动选择是比较麻烦的, 如果直接调用安装, 则系统会弹窗提示.
原创文章, 转载请说明出处~