FileProvider

FileProvider

前言

Android开发始终脱离不了图片处理,特别是Android 7.0开始,无法通过file:///URI来进行在应用之间共享文件,取而代之的是content uri。这样必然增加了开发难度,如必须生成content uri,赋予访问权限,同时暂时没有找到能够通过content uri获取文件大小的方法。同时一些特殊需要,如安装apk、调用相机拍照都需要改成content uri,而无法再直接通过Uri.fromFile(File)获取的URI共享文件。所以认真研究FileProvider是必要的。

下面着重通过官方文档介绍FileProvider的使用:FileProvider

注:本文所用代码在FileProvider中,可进行查阅。

翻译

这里就不在引用原文了,下面是翻译的内容

FileProviderContentProvider的一个特殊子类,为了加强在应用之间安全的分享文件。通过创建content://uri替换file:///uri

content://uri允许授予临时的读写权限。当我们创建一个Intent,其中包含一个content uri,为了能够让对方的应用能够访问到这个content uri,可以使用Intent.setFlags()添加权限。如果收到content uri的是Activity,只要该Activity所在栈处于活动状态,那么这些权限一直会存在。如果收到content uri的是Service,那么Service一直运行,权限就会一直在。

相对于file:///uri,你想控制文件的控制权限,那么你必须修改系统的底层文件权限。你提供的这些权限将对所有的应用有效,一直保留到你更改他们。这种权限控制根本上是不安全的。

提供content uri增加了文件访问权限等级,使得FileProvider成为Android安全基础框架非常重要的部分。

关于FileProvider,通过下面5点介绍:

  1. 定义FileProvider
  2. 指定可用的文件
  3. 为一个文件生成content uri
  4. 为一个uri赋予临时权限
  5. 提供content uri给其他应用

1. 定义FileProvider

因为FileProvider的默认功能就是为文件提供content uri,你不需要定义FileProvider的子类。而是,你在manifest中包含一个FileProvider。为了指定FileProvider组件,在manifest添加一个provider元素。这是android:name属性为android.support.v4.content.FileProviderandroidxandroidx.core.content.FileProvider)。设置android:authoritiescontent uri的域名。例如你的域名是wang.mycroft,你应该设置authoritywang.mycroft.fileprovider。设置android:exported属性为falseFileProvider不需要设置为公开的。设置android:grantUriPermissions属性为true,为了允许赋予文件的临时访问权限。如下:

<manifest>
    ...
    <application>
        ...
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="wang.mycroft.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            ...
        </provider>
        ...
    </application>
</manifest>

如果你想重写FileProvider方法的默认行为,那么继承FileProvider类,在provider中指定android:name为其的全路径类名。

2. 指定可用的文件

一个FileProvider只能为预先指定的文件夹下的文件提供content uri。为了指定一个文件夹,在xml中指定文件的存储路径,在paths下添加子属性。例如,下列的path元素告诉FileProvider你想要把私有文件区域下的image/子目录提供content uri

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="images/"/>
    ...
</paths>

<paths>元素必须包含一个或多个子元素:

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

代表app内部存储区域的files/子目录下的文件。此子目录的值和Context.getFilesDir()的返回值相同。

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

代表app内部存储区域的缓存子目录。此子目录的值和Context.getCacheDir()的返回值相同。

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

代表外部存储区域的根目录。此子目录的值和Environment.getExternalStorageDirectory()的返回值相同。

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

代表app外部存储区域的文件。此子目录的值和Context.getExternalFilesDir(null)的返回值相同。

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

代表app外部存储区域的缓存子目录文件。此子目录的值和Context.getExternalCacheDir()的返回值相同。

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

代表app外部存储区域的多媒体子目录文件。此子目录的值和Context.getExternalMediaDirs()的返回值相同。(Context.getExternalMediaDirs()需要API > 21


这些子元素使用相同的属性:

  1. nameuri相对路径。为了强制保证安全,这个值用于隐藏实际分享的子目录。子目录名包含在path属性上。
  2. path:被分享的目录。name被认为是uri相对路径,path则是实际分享的子目录。注意,path的值是一个子目录,不是具体的文件。不能单独指定一个分享的文件名,也不能使用通配符指定一系列的文件。

一定要将被分享文件的所在目录添加到paths中,作为一个子元素,如下xml中制定了两个子目录:

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="images/"/>
    <files-path name="my_docs" path="docs/"/>
</paths>

paths元素和其子元素添加到项目中的xml文件中。例如将其放在res/xml/file_paths.xml中。为了在FileProvider中引用这个文件,添加一个<meta-data>元素作为我们定义的<provider>的子元素。设置<meta-data>元素的子元素android:name值为android.support.FILE_PROVIDER_PATHS,设置子元素android:resource的属性值为@xml/file_paths(注意不需要添加后缀.xml)。如下:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="wang.mycroft.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

3. 为一个文件生成content uri

为了使用content uri分享一个文件到另外的应用程序,你的app必须生成content uri。为了生成content uri,为这个文件构造一个File对象,传入FileProvider.getUriForFile()方法中,得到一个URI对象。你可以将得到的URI添加到一个Intent中,然后发送到另外的应用程序。收到URI的应用程序,可以通过调用ContentResolver.openFileDescriptor()得到一个ParcelFileDescriptor对象,用于打开文件和获取其中的内容。

例如,假定你的app使用一个FileProvider分享文件到其他app中,authority值为wang.mycroft.fileprovier。为了获取在内部存储区域的images/子目录中的文件default_image.jpgcontent uri,使用如下代码:

File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = FileProvider.getUriForFile(getContext(), "wang.mycroft.fileprovider", newFile);

最后得到的content uri的值是:content://wang.mycroft.fileprovider/images/default_image.jpg

4. 为一个uri赋予临时权限

为了为FileProvider.getUriForFile()得到的content uri赋予访问权限,需要如下步骤:

  1. 为一个content uri调用Context.grantUriPermission(package, Uri, mode_flags),使用期望的标记(flags)。这样就为指定的包赋予了content uri临时的访问权限。标记(flags)可以设置的值为:Intent.FLAG_GRANT_READ_URI_PERMISSION和(或) Intent.FLAG_GRANT_WRITE_URI_PERMISSION。权限保留到你调用revokeUriPermission()或者直到设备重启。
  2. 调用Intent.setData()content uri添加到Intent中。
  3. 调用Intent.setFlags()设置Intent.FLAG_GRANT_READ_URI_PERMISSION和(或) Intent.FLAG_GRANT_WRITE_URI_PERMISSION。最后将Intent发送到另外的app中。大多数时候,你会通过Activity.setResult()使用。

content uriActivity所在的栈保持活跃状态,那么权限就会一直会被保留。当任务栈结束,权限将自动移除。权限会被赋予给Activity所在app的所有组件。

5. 提供content uri给其他应用

会有多种方法将一个content uri提供给其他app。一个通用的方法是通过调用startActivityForResult(),其他应用通过发送一个Intent启动我们appActivity。作为相应,我们的app将直接返回一个content uri给对方的app,或者提供一个界面,让用户选择文件。在后一种情况下,一旦用户选择我们app的文件,我们将提供文件的content uri。在两种情况下,我们的app都会通过setResult()返回带有content uriIntent

你也可以将content uri放在ClipData中。然后将ClipData添加到Intent发送到指定app。通过调用Intent.setClipData()即可。可以在Intent添加多个ClipData。当你调用Intent.setFlags()设置临时权限时,同样的权限将被设置到所有的content uri中。

源码

FileProvider的源码比较简单,反而我觉得应该更多的了解ContentResolverFileDescriptorParcelFileDescriptor的使用,这是FileProvider的基础知识。所以这里不分析源码,后面有机会再深入了解。

使用

举几个我们在开发过程中,实际会遇到的问题。

下面的代码中都是用了Intent.addFlags(int)添加Intent.FLAG_GRANT_READ_URI_PERMISSIONIntent.FLAG_GRANT_WRITE_URI_PERMISSION权限,这样就为Intent中所有的UriClipData赋予了临时权限。另外还有一种方法是使用Context.grantUriPermission(String, Uri)来单独为某一个package(包/app)赋予Uri的访问权限。两者必有其一,不用重复添加。

前提

manifest中添加FileProvider

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="wang.mycroft.fileprovider.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <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"?>
<paths>
    <files-path
        name="image"
        path="image_file" />
    <external-files-path
        name="apk"
        path="apk_file" />
</paths>

1. 下载apk,调用系统安装

因为无法再使用Uri.fromFile(File),所以就必须使用FileProvider。如下所示,得到apk文件的content uri,然后启动Android安装器。另外需要注意,添加安装文件的权限。

private fun installApk(file: File) &#123;
    val uri = FileProvider.getUriForFile(this, "wang.mycroft.fileprovider.fileprovider", file)
    val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
    installIntent.data = uri
    installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

    val packageList = packageManager.queryIntentActivities(installIntent, 0)
    if (packageList.size > 0) &#123;
        startActivity(installIntent)
    &#125;
&#125;
<!-- 请求安装APK的权限,API29舍弃,应该使用PackageInstaller -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

2. 调用相机拍照

下面将app私有文件提供给相机,拍照后进行保存。

private var tempPhotoUri: Uri? = null

private var tempFile: File? = null

private fun takePhoto() &#123;
    tempFile = File(File(filesDir, "image_file"), "$&#123;UUID.randomUUID()&#125;.jpg")
    if (!tempFile?.parentFile?.exists()!! && tempFile?.parentFile?.mkdirs()!!) &#123;
        return
    &#125;
    tempPhotoUri =
        FileProvider.getUriForFile(this, "wang.mycroft.fileprovider.fileprovider", tempFile!!)

    val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    intent.putExtra(MediaStore.EXTRA_OUTPUT, tempPhotoUri)
    intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)

    val packageList = packageManager.queryIntentActivities(intent, 0)
    if (packageList.size > 0) &#123;
        startActivityForResult(intent, 1)
    &#125;
&#125;

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) &#123;
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == 1) &#123;
        // 回收权限
        revokeUriPermission(tempPhotoUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)

        // 有可能返回结果为Activity.RESULT_CANCEL
        if (resultCode == Activity.RESULT_OK) &#123;
            val option = BitmapFactory.Options()
            option.inSampleSize = 4
            image.setImageBitmap(BitmapFactory.decodeFile(tempFile?.absolutePath, option))
        &#125;
    &#125;
&#125;

3. 图片读取并压缩

在如下代码中,读取一个content uri的图片文件,进行压缩,并显示在屏幕上。

private fun compressImageUri(imageUri: Uri) &#123;
    // 打开流
    contentResolver.openInputStream(imageUri)?.let &#123;
        // 仅仅读取尺寸
        val options = BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeStream(it, null, options)

        // 读取到了尺寸,设置inSampleSize
        options.inJustDecodeBounds = false
        val screenWidth = getScreenWidth()
        if (screenWidth != -1) &#123;
            options.inSampleSize = options.outWidth / screenWidth
        &#125;

        // 真正的读取内容
        val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri), null, options)
        bitmap.let &#123;
            // 显示在ImageView上
            image.setImageBitmap(bitmap)
        &#125;
    &#125;
&#125;
private fun getScreenWidth(): Int &#123;
    val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager
    val point = Point()
    wm.defaultDisplay.getRealSize(point)
    return point.x
&#125;

总结

我们在app之间共享内容最多的应该就是图片了,而Android 7.0开始,不允许直接使用file:///URI进行共享,这样会触发FileUriExposedException。取而代之的是使用content uri。避免了文件安全问题,但是也增加了开发成本,当然也是我们必须学习的一环。

FileProvider的使用其实非常简单,难以理解的是Uri的操作。但是作为安卓开发工作者要接受一个比较重要的概念:避免直接使用文件,一切使用Uri来共享内容,并赋予访问权限。


   转载规则


《FileProvider》 Mycroft Wong 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Okio官方文档翻译 Okio官方文档翻译
Okio官方文档翻译Okio是一个辅助java.io和java.nio变得更易于访问、存储、操作数据的库。它一开始是作为Okhttp的组件存在。Okhttp是在Android中非常好用的HTTP客户端。Okio经过充分测试,并且准备好处理新
下一篇 
三、View布局 三、View布局
View布局前言什么是layout布局?前面,我们通过measure测量得到了View的尺寸,那么View到底是放在哪个位置上的呢?这就是layout的功能,确定View在屏幕上的位置(通常是相对于其parent的位置)。 谁来布局不同于m
2019-08-24
  目录