DevWiki

Android App更新安装APK
概要一般地, Android App 都会被要求在App内进行软件更新提示, 让用户下载apk文件, 然后更新安装...
扫描右侧二维码阅读全文
30
2018/11

Android App更新安装APK

概要

一般地, Android App 都会被要求在App内进行软件更新提示, 让用户下载apk文件, 然后更新安装新版本, 一般过程如下:

  1. 检测是否有新版本
  2. 下载新版本app apk文件
  3. 安装新的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中,其中:

<files-path name="name" path="path" />

代表你的app内部存储区域 files/ 目录下的文件, 等同于 Context.getFilesDir().

<cache-path name="name" path="path" />

代表你的app内部存储区域 files/ 目录下的文件, 等同于 Context.getCacheDir().

<external-path name="name" path="path" />

代表外部存储根目录下的文件路径, 等同于 Environment.getExternalStorageDirectory().

<external-files-path name="name" path="path" />

代表你的app外部存储区域根木目录下的路径, 等同于Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)

<external-cache-path name="name" path="path" />

代表你的app外部存储区域缓存目录下的路径, 等同于 Context.getExternalCacheDir()

<external-media-path name="name" path="path" />

代表你的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界面, 让用户手动选择是比较麻烦的, 如果直接调用安装, 则系统会弹窗提示.

原创文章, 转载请说明出处~

Last modification:December 1st, 2018 at 03:46 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment