2

FileProvider

在Android7.0及之后我们无法直接将一个FileUri共享给另一个程序进行使用。系统会抛出一个异常FileUriExposedException。官方是这样描述的:

The exception that is thrown when an application exposes a file:// Uri to another app.

当一个应用程序暴漏一个file:// Uri给另一个app时就会抛出这个异常。

This exposure is discouraged since the receiving app may not have access to the shared path. For example, the receiving app may not have requested the Manifest.permission.READ_EXTERNAL_STORAGE runtime permission, or the platform may be sharing the Uri across user profile boundaries.

由于需要接收fileURI的应用程序可能无法访问共享的路径,因此不建议这样做。这可能是由于使用了Manifest.permission.READ_EXTERNAL_STORAGE权限导致,或者平台可以跨越用户配置边界共享Uri。

PS:这个很好理解,比如说我有一个app被装在了手机上,但是没有申请READ_EXTERNAL_STORAGE权限(6.0后需要动态申请),但是我在另一个程序中请求这个app来读取这个文件是不是就会出现问题了,肯定就会出现异常了。所以说使用了内容提供程序,数据的读取是由内容提供者进行读取的,这样就要求数据提供者必须具有这个权限,也保证了数据安全。

Instead, apps should use content:// Uris so the platform can extend temporary permission for the receiving app to access the resource.

我们应该使用content:// Uris对其进行替换,以便平台可以为需要访问特定资源的app扩展临时权限。

This is only thrown for applications targeting Build.VERSION_CODES#N or higher. Applications targeting earlier SDK versions are allowed to share file:// Uri, but it's strongly discouraged.

这个异常只会在目标版本大于等于7.0时抛出。之前的版本可以继续使用fileURI,不过不推荐这样做。

这些都是由于7.0开启了严格模式(StrictMode)造成的,官方在7.0的变更中是这么说的:

对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。

FileProvider类的继承关系

java.lang.Object
       android.content.ContentProvider
            android.support.v4.content.FileProvider

官方介绍

FileProvider is a special subclass of ContentProvider that facilitates secure sharing of files associated with an app by creating a content:// Uri for a file instead of a file:/// Uri.

FileProviderContentProvider的子类,它通过为一个文件创建content:// Uri 来替换file:/// Uri,以此来达到文件的安全共享。

核心步骤

1、定义FileProvider

2、定义可用的文件路径

3、为定义的FileProvider添加文件路径

4、为特定文件生成ContentURI

5、授予ContentURI授予临时权限

1、定义FileProvider

由于FileProvider提供了ContentURI的生成方法,所以我们无需在代码中定义写一个它的子类。以下代码中的name属性是固定的,authorities可以自己定义,一般是包名字加上.fileprovider。exported设置为false,因为通常是拒绝外部直接访问的。grantUriPermissions需要为true,需要授予临时的Uri权限。

<manifest>
    ...
    <application>
        ...
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.mydomain.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            ...
        </provider>
        ...
    </application>
</manifest>

2、定义可用的文件路径

FileProvider只能为预先指定的目录中的文件生成可用的ContentURI。要指定目录,需要使用<paths>

该文件需要建立在res目录下名为xml的目录下,xml目录需要自己建立。

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!--定义APP的存放目录-->
    <external-path
        name="AppInstaller"
        path="/Download"></external-path>
</paths>

paths下可以包含一个或者多个子节点。

<root-path/> 代表设备的根目录new File("/");//很少用
//app内部存储
<files-path/> 代表context.getFilesDir()
<cache-path/> 代表context.getCacheDir()
//sd卡存储
<external-path/> 代表Environment.getExternalStorageDirectory()
<external-files-path>代表context.getExternalFilesDirs()
<external-cache-path>代表getExternalCacheDirs()

我们还可以在path中用.代替所有目录。

3、为定义的FileProvider添加文件路径

这里我们加入刚才添加的path文件,注意meta-data中的name项必须是android.support.FILE_PROVIDER_PATHS。

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

记不住这个name怎么办?好上头!!!!懒人总是有办法。在FileProvider类的内部正好有一个定义可供我们Copy。

private static final String META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS";

4、为特定文件生成ContentURI

FileProvider提供了getUriForFile函数帮助我们生成ContentURI。这里需要注意的是我们使用的文件路径必须是前边在path中定义的。否则要path何用....。

第一个参数为context,第二个是定义的provider中设置的authorities,第三个是一个File对象。

//文件路径
File file = 
    new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getCanonicalPath()
        + "/apps/MyApp.apk");
//获取文件对应的content类型Uri
Uri uri = FileProvider.getUriForFile(this, "com.mydomain.fileprovider", file);

观察我们生成的Uri示例,上边是我们普通的fileUri下边是我们生成的ContentUri,区别就在于ContentUri没有暴露具体的文件路径。

//普通的fileUri(通过Uri.fromFile(file)获取)
file:///storage/emulated/0/Download/apps/MyApp.apk
//contentUri
content://com.qylost.fileproviderdemo.fileprovider/AppInstaller/MyApp.apk

常见使用场景

1、跨程序共享文件

以下我们通过两个app演示两个程序使用FileProvider共享数据。提供数据的被称为:ServerApp,接受数据的被称为:ClientApp。

ServerApp:

主要是如上所说的在Manfiest中定义provider,以及定义共享路径。

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.qylost.fileproviderdemo.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/my_path"></meta-data>
</provider>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path
        name="ShareToMyApp"
        path="."></files-path>
</paths>

ClientApp:

这里我们新增了一个Main2Activity,在这里读取ServerApp通过FileProvider传来的数据。

public class Main2Activity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        Intent intent = getIntent();
        if (intent != null && intent.getData() != null) {
            try {
                ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(intent.getData(), "r");
                FileReader reader = new FileReader(parcelFileDescriptor.getFileDescriptor());
                BufferedReader bufferedReader = new BufferedReader(reader);
                String res = new Scanner(bufferedReader).useDelimiter("\\A").next();//解析传来的数据
                Toast.makeText(this, res, Toast.LENGTH_SHORT).show();//弹出
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}

这里加入intent-filter,定义了action的名称,以及mimeType,这个在请求的时候需要用到。注意category不可少。

<activity android:name=".Main2Activity">
    <intent-filter>
        <data android:mimeType="share/text" />
        <action android:name="com.qylost.fileproviderdatareceverdemo.SHARE"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>

在ServerApp中调用如下代码,共享数据:

//在files目录下写入测试数据
writeTestData();//这里在内部files文件目录下写入了文本内容Hello File Provider!文件名为:FileProviderTest.txt
//开始共享数据
File file = new File(getFilesDir(), "FileProviderTest.txt");
Uri uri = FileProvider.getUriForFile(this, "com.qylost.fileproviderdemo.fileprovider", file);
Intent intent = new Intent("com.qylost.fileproviderdatareceverdemo.SHARE");//这个就是在上边配置intent-filter时设置的action name
intent.setDataAndType(uri, "share/text");//在上边intent-filter中设置的mimeType
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//授予临时读取权限
startActivity(intent);

效果图:

2、打开App安装程序

//文件路径
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getCanonicalPath() + "/MyApp.apk");
Intent intent = new Intent(Intent.ACTION_VIEW);
//获取文件对应的content类型Uri
Uri uri = FileProvider.getUriForFile(this, "com.qylost.fileproviderdemo.fileprovider", file);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
//intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//可以不加
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);

3、拍照

//定义文件名称
String fileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".jpg";
String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getCanonicalPath() 
    + "/" + fileName;
//获取文件的ContentURI
File file = new File(path);
Uri uri = FileProvider.getUriForFile(this, "com.qylost.fileproviderdemo.fileprovider", file);
//定义Intent对象
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action为MediaStore下的ACTION_IMAGE_CAPTURE
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);//设置Extra标志为输出类型
intent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);//授予临时权限
startActivityForResult(intent, 1);

//接收拍照结果
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    //拍照成功(这里可以将请求拍照的File对象定义为成员变量,这样成功后就可以拿到图片了)
    if (requestCode == 1 && resultCode == RESULT_OK) {
        Toast.makeText(this, "Success", Toast.LENGTH_SHORT).show();
    }
    super.onActivityResult(requestCode, resultCode, data);
}

基本工作原理

使用fileUri的工作流程图:

1、A共享文件绝对路径给B

2、B通过路径读取数据

通过fileUri共享文件简单粗暴,直接将路径进行共享,这样做会存在一些问题:

1、文件路径暴露。

2、这个文件路径可能是一个外部存储路径(外部存储路径需要申请权限,可能App B没有这个权限,就会出现异常。再或者AppA没有外部存储读写权限,那么将文件读取交给了一个具有外部存储读写权限的App就会存在安全隐患)。

为了解决这两个问题,所以使用contentURI,使用“相对“路径解决路径暴露问题,数据读取是交由提供者来完成的。

使用ContentUri的工作流程图:

A仅仅给B分享了ContentURI,具体的文件读取是由内容/数据提供方(App A)来完成的,App B只能去问App A拿数据。

1、A共享ContentURI给B

2、B拿着这个URI找A要数据

3、A读取文件中的数据给B

手动关闭严格模式

不推荐这么来搞,不过还是要知道的。

//手动关闭严格模式
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
builder.detectAll();
StrictMode.setVmPolicy(builder.build());

参考文献

1、https://developer.android.com...

2、https://blog.csdn.net/chen_wh...

3、https://blog.csdn.net/Next_Se...

4、https://developer.android.com...(需要梯子)


qylost
3 声望2 粉丝

水之积也不厚,则其负大舟也无力;风之积也不厚,则其负大翼也无力。