Android 7.0中FileProvider

Android7.0中增加了一些新特性,也对系统安全性进行了提高,具体增加了那些新特性大家可以参考Android的官方文档。

Android7.0新特新

这篇文字我们来说一说对于我们开发者最重要的一项改变。那就是在应用之间共享文件。

对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。

我记得第一次关注到FileProvider是又一次很多用户反馈,应用不能自动更新,当安装包下载完成后,一调用安装程序就crash了,当时我很纳闷,查了下后台日志,发现了问题。

当apk下载完成后,我们会调用下面的代码去让用户安装apk:

Uri uri = Uri.fromFile(new File("/sdcard/demo.apk"));
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(uri, "application/vnd.android.package-archive");

一旦调用这个就应用就crash,查看logcat异常如下:

Caused by: android.os.FileUriExposedException: file:///sdcard/demo.apk exposed beyond app through Intent.getData()
                                                                          at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
                                                                          at android.net.Uri.checkFileUriExposed(Uri.java:2346)
                                                                          at android.content.Intent.prepareToLeaveProcess(Intent.java:8951)
                                                                          at android.content.Intent.prepareToLeaveProcess(Intent.java:8910)

那这个异常是在哪里报的呢?
是在StrictMode.java中报出来的:

public static void onFileUriExposed(Uri uri, String location) {
        final String message = uri + " exposed beyond app through " + location;
        if ((sVmPolicyMask & PENALTY_DEATH_ON_FILE_URI_EXPOSURE) != 0) {
            throw new FileUriExposedException(message);
        } else {
            onVmPolicyViolation(null, new Throwable(message));
        }
}

这个方法是在Uri中调用的,调用代码如下:

public void checkFileUriExposed(String location) {
        if ("file".equals(getScheme()) && !getPath().startsWith("/system/")) {
            StrictMode.onFileUriExposed(this, location);
        }
}

看见没,这里有一个判断,如果我们uri中的schem是file的话,那么就会报出这个异常。既然我们已经知道了问题所在了,那如何解决这个问题呢?

使用FileProvider

FileProvider其实是ContentProvider的子类,它的作用也比较明显了,file:///Uri不给用,那么换个Uri为content://来替代。使用步骤如下

(1)注册FileProvider

首先我们需要在AndroidManifest.xml中注册FileProvider,注册代码如下:

<provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.wms.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_path"/>
provider>

注意上面的注册代码中要一个xml文件,我们下面来看看xml文件应该怎么编写

(2)编写xml文件


<resources>
    <paths>
        <root-path name="name" path=""/>
        <files-path name="name" path="" />
        <cache-path name="name" path="" />
        <external-path name="name" path="" />
        <external-files-path name="name" path="" />
        <external-cache-path name="name" path="" />
    paths>
resources>
  1. 代表的是系统根目录,类似new File(“/”)
  2. 代表的是Context.getFilesDir()
  3. 代表的是Context.getCacheDir()
  4. external-path 代表的是Environment.getExternalStorageDirectory()
  5. external-files-path 代表的是 Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
  6. 代表的是 Context.getExternalCacheDir()

上面这六个子元素都有相同的属性
1. name属性: 这个属性可以随便指定名称
2. path属性: 代表上面目录的子目录,不能指定为一个当个文件,必须是目录,例如:

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

如果你在xml中指定为这段代码,那么代表的是/sdcard/test 目录

为File生成Content URI

要使用content:// 与另外一个应用共享一个文件,你的应用不得不生成content uri,通俗的来讲就是我们需要将uri转化成content://。FileProvider中提供了方法供我们调用。代码如下:

Uri uri = Uri.fromFile(new File("/sdcard/demo.apk"));
Intent intent = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= 24) {
    uri = FileProvider.getUriForFile(this, "com.wms.fileprovider", new File("/sdcard/demo.apk"));
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Log.e(TAG, "onCreate: uri = " + uri.toString());
intent.setDataAndType(uri, "application/vnd.android.package-archive");
startActivity(intent);

这时候打印的uri如下:

content://com.wms.fileprovider/my_file/demo.apk

FileProvider.getUriForFile() 里面一共传3个参数,第一个是C ontext, 第二个是我们在AndroidManifest.xml中声明的authorities,第三个参数是我们要转换的File。

有没有注意到上面代码中我们加了一行:

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

如果不加这行代码,我们看下logcat中会跑出异常,但是程序不会crash掉。但是加上这行代码,就没有警告了。

java.lang.SecurityException: Permission Denial: opening provider android.support.v4.content.FileProvider from ProcessRecord{c9a8125 5564:com.miui.packageinstaller/u0a44} (pid=5564, uid=10044) that is not exported from uid 10134
                                                       at android.os.Parcel.readException(Parcel.java:1683)
                                                       at android.os.Parcel.readException(Parcel.java:1636)
                                                       at android.app.ActivityManagerProxy.getContentProvider(ActivityManagerNative.java:4191)
                                                       at android.app.ActivityThread.acquireProvider(ActivityThread.java:5528)

鉴于上面出现的问题,我们引出FileProvider的授权问题。

FileProvider授权

为了给getUriForFile() 授予访问content uri权限,我们应该遵循下面几个步骤:

  1. content:// Uri 调用 Context.grantUriPermission(package, Uri, mode_flags),使用希望的模式标志。这种方式根据mode_flags授予制定的程序临时访问的权限。mode_flags可以设定为 FLAG_GRANT_READ_URI_PERMISSION 或者 FLAG_GRANT_WRITE_URI_PERMISSION,或者两者都有。这种授权一直会存在,直到你调用 revokeUriPermission() 或者手机重启。

  2. 调用 setData将content URI放入Intent 当中

  3. 调用 Intent.setFlags() 设置 FLAG_GRANT_READ_URI_PERMISSION 或者 FLAG_GRANT_WRITE_URI_PERMISSION 权限。

  4. 发送Intent给其他应用,大多数的时候是调用setResult

上面这种授权的方式很麻烦,建议在使用FileProvider的时候判断下系统版本就ok了。

你可能感兴趣的:(android开发)