Android APP自动升级安装失败

Android APP自动升级安装失败

概述

自动升级在APP中是一个非常常见的功能,当你的应用有更新时,可以提醒用户升级甚至在必要时可强制用户升级。但随着系统版本的更新,安装apk的权限也在收紧,导致一些APP在高版本的机器上升级失败。这时就有必要了解一下如何处理这样的问题了。

权限机制

在Android7.0的发布介绍中提到了一些文件系统权限方面的修改。官网:https://developer.android.google.cn/about/versions/nougat/android-7.0-changes

以下是官网的译文(用google翻译的网页)

权限更改

Android 7.0包含可能会影响您的应用的权限更改。

文件系统权限更改

为了提高私人文件的安全性,针对Android 7.0或更高版本的应用的私人目录限制了访问权限(0700)。此设置可防止私有文件的元数据泄漏,例如其大小或存在。此权限更改有多种副作用:

所有者不应再放宽私有文件的文件权限,并且使用MODE_WORLD_READABLE和/或 尝试执行此操作 MODE_WORLD_WRITEABLE将触发a SecurityException。

注意:截至目前,此限制尚未完全执行。应用程序仍可使用本机API或FileAPI 修改其私人目录的权限。但是,我们强烈建议不要放宽对私人目录的权限。

file://在包域外 传递URI可能会使接收者无法访问路径。因此,尝试传递 file://URI触发器a FileUriExposedException。共享私有文件内容的推荐方法是使用FileProvider。

该DownloadManager可以通过文件名不再私下共享存储的文件。传统应用程序在访问时可能会以无法访问的路径结束COLUMN_LOCAL_FILENAME。针对Android 7.0或更高版本的应用会SecurityException在尝试访问时 触发COLUMN_LOCAL_FILENAME。通过使用DownloadManager.Request.setDestinationInExternalFilesDir()或 DownloadManager.Request.setDestinationInExternalPublicDir() 仍然可以访问路径 来将下载位置设置为公共位置的旧应用程序 COLUMN_LOCAL_FILENAME,但强烈建议不要使用此方法。访问由文件公开的文件的首选方法DownloadManager是使用 ContentResolver.openFileDescriptor()。

在应用之间共享文件

对于定位到Android 7.0的应用,Android框架会强制执行StrictModeAPI策略,禁止file://在应用外部公开URI。如果包含文件URI的intent离开您的应用程序,则该应用程序将失败并显示FileUriExposedException异常。

要在应用程序之间共享文件,您应该发送content://URI并授予URI临时访问权限。授予此权限的最简单方法是使用FileProvider该类。有关权限和共享文件的详细信息,请参阅共享文件。

大意就是说文件的访问权限提高了,不能直接使用file://的方式来共享文件了,应该使用
content://URI的方式来共享文件,并使用FileProvider类来授权。

应对方法

老代码一般都是用下面这样的代码来安装下载下来的apk的:

 public static void installAPk(Context context, File apkFile) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        Uri uri = Uri.fromFile(apkFile);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
        context.startActivity(intent);
    }

但7.0以上要求使用FileProvider来授权访问文件。

根据官方的指引:https://developer.android.google.cn/reference/android/support/v4/content/FileProvider,大概需要以下几个步骤:

  1. 定义FileProvider
  2. 指定可用文件
  3. 生成文件的URI
  4. 授予URI临时权限
  5. 向另一个应用程序提供内容URI

下面来一一介绍一下这几个步骤:

一、定义FileProvider

在AndroidManifest.xml文件中注册provider

<provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.xbd.file.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <!-- 元数据 -->
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/update_apk_paths" />
</provider>

解释一下几个参数的含义:

android:name
文件提供者的类名,固定为"android.support.v4.content.FileProvider",如果你很牛逼也可以自己写一个类并继承"android.support.v4.content.FileProvider",然后实现一些扩展的功能。

android:authorities
权限的名字,用于标识provider提供的内容,可以有多个名字,各名字之间用“;”隔开。为了不和其它名字冲突一般使用域名的形式来描述

android:exported
内容提供者是否可供其他应用程序使用,在这里不需要,所以填false

android:grantUriPermissions
是否授权给那些本来无权限访问的人临时访问内容提供者提供的内容,这里填true,不然就没法访问到这个文件了。

二、指定可用的文件

为了指定需要访问的文件,需要在一个xml文件中指定被访问文件的存储路径。
在res目录下新建一个xml文件夹,然后新建一个文件:update_apk_paths.xml(文件名自己随意起),内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
    <!--
    files-path:          该方式提供在应用的内部存储区的文件/子目录的文件。
                          它对应Context.getFilesDir返回的路径:eg:”/data/data/com.jph.simple/files”。

    cache-path:          该方式提供在应用的内部存储区的缓存子目录的文件。
                          它对应getCacheDir返回的路径:eg:“/data/data/com.jph.simple/cache”;

    external-path:       该方式提供在外部存储区域根目录下的文件。
                          它对应Environment.getExternalStorageDirectory返回的路径:

    external-cache-path: 该方式提供在应用的外部存储区根目录的下的文件。
                          它对应Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
                          返回的路径。eg:”/storage/emulated/0/Android/data/com.jph.simple/files”
    -->
    <cache-path name="update" path="" />
</paths>
</resources>

paths元素下可以有很多子元素,如files-path、cache-path、external-path等,意义在上面的注解中都说明了。这里我使用了 cache-path,也就是说我的apk文件存放在了内部存储区的缓存目录中。

name=“update”
相当于下面的path的别名,为了把真实的路径隐藏起来,这样就只能看到别名,如果按照这个别名路径去找文件的话肯定是找不到的。这个别名自己随便取,我把它叫做“update”

path=""
代表你要分享的真实的子目录名,空字符串代表根目录,注意该值必须是一个子目录,不能是文件名

综合来讲,以上配置表明:我要分享一个目录供其它人访问,这个目录就是内部存储区的缓存目录的根目录,即 getCacheDir()的返回值。所有根目录及其子目录下的文件都可以被访问,同时我为这缓存目录取了一个别名叫“update”,以混淆视听。

然后将上面的update_apk_paths.xml文件链接到AndroidManifest.xml中定义的provider中,也就是定义中的“元数据”的内容

<!-- 元数据 -->
<meta-data
    android:name="android.support.FILE_PROVIDER_PATHS"
    android:resource="@xml/update_apk_paths" />	

android:name
代表资源的类型,此处为固定值"android.support.FILE_PROVIDER_PATHS"

android:resource
代表资源文件,即update_apk_paths.xml,但是不要后缀名

三、生成文件的URI

用以下方式生成文件的Uri:

Uri apkUri = FileProvider.getUriForFile(context, "com.xbd.file.provider", apkFile);

其中,第二个参数"com.xbd.file.provider"是在AndroidManifest.xml文件中声明的provider中 android:authorities元素的值,第三个参数apkFile就是下载下来的保存在缓存目录下的apk文件

四、授予URI临时权限

授权有很多种方式:这里只说一种,就是通过Intent addFlags()方法,如:

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

五、向另一个应用程序提供内容URI

用startActivity(intent)启动一个应用就可以了,被启动的应用就有权限访问你提供的文件了,但要注意必须添加这句:

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

实用代码

综合以上分析,可将原来安装apk的代码改成以下的样子:

public static void installAPk(Context context, File apkFile) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        // 7.0 以上
        Uri apkUri = FileProvider.getUriForFile(context, "com.xbd.file.provider", apkFile);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    } else {
        // 7.0以下
        Uri uri = Uri.fromFile(apkFile);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
    }
    context.startActivity(intent);
}

当然,还要有前面介绍的配置一起配合使用。



由于水平有限,如果文中存在错误之处,请大家批评指正,欢迎大家一起来分享、探讨!

博客:http://blog.csdn.net/MingHuang2017

GitHub:https://github.com/MingHuang1024

Email: [email protected]

微信:724360018

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章