Android-0.FileProvider简介解决android.os.FileUriExposedException

本文详细解析了在Android 7.0及以上版本中遇到的FileUriExposedException异常,介绍了如何使用FileProvider将file://URI转换为content://URI,以避免应用在跨应用共享文件时出现崩溃问题。
摘要由CSDN通过智能技术生成

报错

android.os.FileUriExposedException: 
file:///storage/emulated/0/Movies/VID_88.mp4 exposed beyond app through Intent.getData()

原因

从 7.0 开始,Android SDK 中的 StrictMode 策略禁止开发人员在应用外部公开 file:// URI。具体表现为,当我们在应用中使用包含 file:// URIIntent 离开自己的应用时,程序会发生故障。
开发中,如果我们在使用 file:// URI 时忽视了这两条规定,将导致用户在 7.0 及更高版本系统的设备中使用到相关功能时,出现 FileUriExposedException 异常,导致应用出现崩溃闪退问题。而替代解决方案便是使用 FileProvider
FileProviderContentProvider 的一个特殊子类,帮助我们将访问受限的 file:// URI 转化为可以授权共享的 content:// URI

解决方案

1.在mainfest中注册一个FileProvider

    <application
        ......
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>
    </application>

android:exported="false":是否设置为独立进程。
android:grantUriPermissions="true" :是否拥有共享文件的临时权限。
android:resource="@xml/provider_paths" :共享文件的文件根目录xml配置。
android:authorities="${applicationId}.provider":属性值是一个由 build.gradle 文件中的 applicationId 值和自定义的名称.provider组成的 Uri 字符串(这样写是约定俗成的)。其他属性值使用如上固定值即可。

2.添加共享目录

由前面的android:resource="@xml/provider_paths" 配置指定,我们需要在Res下新建一个xml文件夹,并在xml文件夹下新建provider_paths.xml,如下图:

内容类似如下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external_files" path="."/>
</paths>

files-path:内部存储空间应用私有目录下的 files/ 目录,等同于 Context.getFilesDir()
cache-path:内部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getCacheDir()
external-path:外部存储空间根目录,等同于 Environment.getExternalStorageDirectory()
external-files-path:外部存储空间应用私有目录下的 files/ 目录,等同于 Context.getExternalFilesDir(null)
external-cache-path:外部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir()
可以看出,这五种子元素基本涵盖内外存储空间所有目录路径,包含应用私有目录。同时,每个子元素都拥有 namepath 两个属性。
path:用于指定当前子元素所代表目录下需要共享的子目录名称。注意:path 属性值不能使用具体的独立文件名,只能是目录名。
name:用于给 path 属性所指定的子目录名称取一个别名。后续生成 content:// URI 时,会使用这个别名代替真实目录名。这样做的目的,很显然是为了提高安全性。

例如:/storage/emulated/0/Movies/1.mp4
在上面的xml配置中
会转换成content://${applicationId}.provider/external_files/Movies/1.mp4
external_files为xml中配置的name

3.生成content Uri

调用FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".provider", new File(path));生成Uri
特别注意的是第二个参数需要和android:authorities="${applicationId}.provider"完全一致。

4.授予Content Uri访问权限

生成 Content URI 对象后,需要对其授权访问权限。授权方式有两种:

第一种方式,使用 Context 提供的 grantUriPermission(package, Uri, mode_flags) 方法向其他应用授权访问 Uri 对象。三个参数分别表示授权访问 Uri 对象的其他应用包名,授权访问的 Uri 对象,和授权类型。其中,授权类型为 Intent 类提供的读写类型常量:

FLAG_GRANT_READ_URI_PERMISSION
FLAG_GRANT_WRITE_URI_PERMISSION

这种形式的授权方式,权限有效期截止至发生设备重启或者手动调用 revokeUriPermission() 方法撤销授权时。

第二种方式,配合 Intent 使用。通过 setData() 方法向 intent 对象添加 Content Uri。然后使用 setFlags() 或者 addFlags() 方法设置读写权限,可选常量值同上。这种形式的授权方式,权限有效期截止至其它应用所处的堆栈销毁,并且一旦授权给某一个组件后,该应用的其它组件拥有相同的访问权限。

示例代码如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String path = "/storage/emulated/0/Movies/VID_20181024_105332188.mp4";
                viewMediaFile(path);
            }
        });
    }

    private void viewMediaFile(String path) {
        if (!TextUtils.isEmpty(path)) {
            String type = "video/*";
            if (path.contains("png") || path.contains("jpeg") || path.contains("jpg")) {
                type = "image/*";
            }
            String value = String.valueOf(path);
            File file = new File(value);

            Intent intent = new Intent();
            intent.setAction(android.content.Intent.ACTION_VIEW);
            if (Build.VERSION.SDK_INT >= 24) {
                Uri photoURI = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".provider", file);
                intent.setDataAndType(photoURI, type);
                intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            }
            else {
                intent.setDataAndType(Uri.fromFile(file), type);
            }
            startActivity(intent);
        }
    }
}

最终SDK>=24时/storage/emulated/0/Movies/VID_20181024_105332188.mp4对应Uri
为: content://${applicationId}.provider/external_files/Movies/VID_20181024_105332188.mp4

播放视频的测试代码:

        String path = Environment.getExternalStorageDirectory().getPath()+ "//ahgy413/input.mp4";//该路径可以自定义

        Intent intent = new Intent();
        intent.setAction(android.content.Intent.ACTION_VIEW);
        File file = new File("" + path);
        Uri uri;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            Uri contentUri = FileProvider.getUriForFile(this, "lulubox.provider", file);
            intent.setDataAndType(contentUri, "video/*");
        } else {
            uri = Uri.fromFile(file);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.setDataAndType(uri, "video/*");
        }

        startActivity(intent);

如果遇到打不开视频,报错-10102,记得检查下应用是否被授予权限。也就是可读写内存权限

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值