FileProvider详解,看了绝对不会失望

  • 起因

FileProvider,goole在Android7.0对权限的升级推出的,但是学习中还是缺少思考。
今天测试丢过来一个bug,我一排查是关于这个FileProvider的,改问题中我突然心里问自己一个问题,FileProvider用过好多次了,它用来解决什么问题的?自己瞬间懵…感觉Android7.0以上好像就得用它(其实并非这么回事),我问同事同样的问题,把他也问懵了?。所以决定把它拿出来讲一讲。

  • FileProvider为什么被设计出来?
为了提高私有目录的安全性,防止应用信息的泄漏,从 Android 7.0 开始,应用私有目录的访问权限被做限制。具体表现为,开发人员不能够再简单地通过 file:// URI 访问其他应用的私有目录文件或者让其他应用访问自己的私有目录文件。
同时,也是从 7.0 开始,Android SDK 中的 StrictMode 策略禁止开发人员在应用外部公开 file:// URI。具体表现为,当我们在应用中使用包含 file:// URI 的 Intent 离开自己的应用时,程序会发生故障。
开发中,如果我们在使用 file:// URI 时忽视了这两条规定,将导致用户在 7.0 及更高版本系统的设备中使用到相关功能时,出现 FileUriExposedException 异常,导致应用出现崩溃闪退问题。而这两个过程的替代解决方案便是使用 FileProvider。
复制代码
  • 私有目录和公共目录

上面的关键点:应用程序的私有目录。
私有目录:data/app/包名” 和 “data/data/包名。随着app卸载被删除。
公共目录:包括外部公共目录、外部私有目录、缓存目录

 私有目录:随着用户删除app而删除
         context.getCacheDir(); /data/data/包名/cache 
         context.getFilesDir(); /data/data/包名/files
         注意:Android7.0以上区分用户目录是这个: /data/user/0/包名/cache、/data/user/0/包名/files,这个不用太在意它,其实指向的还是data/data目录。
复制代码
外部私有目录:随着用户删除app而删除
        context.getExternalFilesDir(type); /storage/emulated/0/Android/data/包名/files
        context.getExternalCacheDir(); /storage/emulated/0/Android/data/包名/cache
复制代码
外部公共目录:
        Environment.getExternalStoragePublicDirectory(type);
        /storage/emulate/0/....
        有九种type,分别对应不同的目录,如下:

        DIRECTORY_MUSIC:音乐类型    /storage/emulate/0/music
        DIRECTORY_PICTURES:图片类型
        DIRECTORY_MOVIES:电影类型
        DIRECTORY_DCIM:照片类型,相机拍摄的照片视频都在这个目录(digital camera in memory) 
        DIRECTORY_DOWNLOADS:下载文件类型   /storage/emulate/0/downloads
        DIRECTORY_DOCUMENTS:文档类型
        DIRECTORY_RINGTONES:铃声类型
        DIRECTORY_ALARMS:闹钟提示音类型
        DIRECTORY_NOTIFICATIONS:通知提示音类型
复制代码
外部根目录:
        Environment.getExternalStorageDirectory();/storage/emulate/0
复制代码
  • FileProvider使用
1.第一步

<provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true"> //android:grantUriPermissions="true"允许它授予 Uri 临时的权限。
           
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_path"/>
        </provider>
2.第二步 :在res/xml 目录下添加共享目录标识文件file_path

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <root-path
        name="root"
        path="" />
    //代表设备的根目录new File("/");

    <files-path
        name="files"
        path="." />
    //context.getFilesDir()

    <cache-path
        name="cache"
        path="." />
    //context.getCacheDir()

    <external-path
        name="external"
        path="." />
    //Environment.getExternalStorageDirectory()

    <external-files-path
        name="external_file_path"
        path="." />
    //ContextCompat.getExternalFilesDirs()

    <external-cache-path
        name="external_cache_path"
        path="." />
    //ContextCompat.getExternalCacheDirs()
</paths>
//具体对应关系记不住可以查看FileProvider源码。

3.第三步
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        Uri uri = FileProvider.getUriForFile(context, "applicationId.fileProvider", file);
    } else {
        Uri uri = Uri.fromFile(file);
    }
复制代码
  • 示例代码(小伙伴们一定要看哈,我的踩坑记录,太难了。。。)
    1.提供共享目录方:(例如 appA)
    File cacheDir = getCacheDir();
        File file = new File(cacheDir,"bb.txt");
        if(!file.exists()){
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        Uri uri;
        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
            uri = FileProvider.getUriForFile(this, getApplication().getPackageName() + ".fileprovider", file);
        }else {
            uri = Uri.fromFile(file);
        }
        System.out.println("uri.getPath() = " + uri.getPath());

        Intent intent = new Intent();
        intent.setAction("${你的action}");
        
        /**
           * 要支持运行介于Android 4.1(API级别16)和Android 5.1(API级别22)之间的版本的设备,请ClipData从内容URI创建对象,然后在该ClipData对象上设置访问权限.
           这是官方文档标注的,但是我在7.0系统,不加这一句一直没有权限。 官网文档也是大坑。
        */
        intent.setClipData(ClipData.newRawUri("", uri));//这一个一定要加上?
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION| FLAG_GRANT_WRITE_URI_PERMISSION);//允许临时的读和写
        intent.setDataAndType(uri,"*/*");
        startActivity(intent);
        
    2.处理方(另一个app,例如 appB)
    Intent intent = getIntent();
        Uri myUri = intent.getData();
        if(myUri == null) return;
        try {
            OutputStream outputStream = getContentResolver().openOutputStream(myUri);
            outputStream.write('a');
            outputStream.close();
            System.out.println("写入成功");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("e.getMessage() = " + e.getMessage());
        }   
复制代码
  • 总结
Android7.0使用content://替代file://,主要需要FileProvider的支持,而因为FileProvider是ContentProvider的子类,所以需要在AndroidManifest.xml中注册;而又因为需要对真实的filepath进行映射,所以需要编写一个xml文档,用于描述可使用的文件夹目录,以及通过name去映射该文件夹目录。

对于权限,有两种方式:

方式一为Intent.addFlags,该方式主要用于针对intent.setData,setDataAndType以及setClipData相关方式传递uri的。
方式二为grantUriPermission来进行授权

方式一较为简单,对于intent.setData,setDataAndType正常使用即可,但是对于setClipData,由于5.0前后Intent#migrateExtraStreamToClipData,代码发生变化,需要注意~

复制代码

最后欢迎大家提意见,感觉喜欢的点个赞吧,谢谢各位。

参考:Android 7.0 行为变更 通过FileProvider在应用间共享文件吧

Android系统中内部存储和外部存储(公有目录、私有目录、缓存目录)详解

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享