Android 进阶——Android 7.0及以上版本适配之FileProvider跨应用共享文件详解

版权声明:本文为博主原创文章,遵循 CC 4.0 BY 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/CrazyMo_/article/details/102569375

引言

Android 7.0强制启用了所谓的StrictMode的策略,给我们开发者带来最直接的影响就是我们的App无法直接对外暴露file://类型的URI了,若还是通过Uri.fromFile(file)方式来暴露的话就会引发FileUriExposedException
在这里插入图片描述
打开系统相机、录像等时需要使用Intent携带这种类型的URI去实现

//Android 7.0之前
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mVideo));
//Android 7.0及以上
Uri videoUri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", mVideo);

一、FileProvider概述

FileProvider 继承自ContentProvider,它通过为文件创建content://类型(而非低版本file://类型的URI)的URI来促进与应用程序关联的文件的安全共享。因为对于Android 7.0应用(仅仅对于android 7.0版本的SDK而言,若编译版本低于Api 25则不会受到影响),通过StrictMode Api禁止我们的应用对外部(跨越应用分享)对外暴露file://类型的URI,Android 7.0应用间的文件共享需要使用content://类型的URI分享且需要为其提供临时的文件访问权限(Intent.FLAG_GRANT_READ_URI_PERMISSIONIntent.FLAG_GRANT_WRITE_URI_PERMISSION),而android.support.v4.content.FileProvider正是解决这个问题的官方答案。

二、FileProvider的使用步骤

1、在manifest中声明FileProvider

  • name——配置provider的类名,可以配置android.support.v4.content.FileProvider使用默认的v4的FileProvider,也可以配置为自定义的继承FileProvider的provider类(配置全类名)。
  • authorities——作为“签名认证”,也可以自定义任意格式的字符串,只需要在代码中获取uri时保持一致,一般传递${applicationId}.fileprovider
  • grantUriPermissions——使用FileProvider的使用需要我们给暴露的URI赋予临时访问权限(READ和WRITE),设置为true才可以具有临时权限
  • exported——false表示对应的provider不需要对外开放。
  • meta-data——配置的是可以访问的文件的路径信息,使用自定义的xml文件进行配置,FileProvider会自动解析xml文件获取配置信息,其中name配置为android.support.FILE_PROVIDER_PATHS,resource为xml对应的路径
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/export_paths" />
</provider>

ContentProvider是Android中提供的专门用于不同应用间数据交互和共享的组件。ContentProvider实际上是对SQLiteOpenHelper的进一步封装,以一个或多个表的形式将数据呈现给外部应用,通过Uri映射来选择需要操作数据库中的哪个表,并对表中的数据进行增删改查处理。ContentProvider其底层使用了Binder来完成APP进程之间的通信,同时使用匿名共享内存来作为共享数据的载体。ContentProvider支持访问权限管理机制,以控制数据的访问者及访问方式,保证数据访问的安全性。

2、定义用于配置给其他应用访问的路径信息的xml文件

FileProvider只能为预先指定的目录中的文件生成内容URI(A FileProvider can only generate a content URI for files in directories that you specify beforehand),所以需要首先在项目res目录下新建xml目录并创建xml文件(名称任意),并通过< paths>的子节点以XML格式指定其存储区和路径,FileProvider自动解析这个xml文件来为对应的文件生成URI,xml文件里可以定义7个子节点,每个子节点里可以配置两个属性:

  • name——对应URI中的路径部分的值,可以任意值

  • path——配置暴露出去目录的“相对路径”,而完整路径取决于当前的子节点类型,也可以配置.

注意:path并非IO意义上的路径,而是ContentProvider使用时候根据业务定义的“路径”,所以这个xml文件其实是用于定义FileProvider对应业务的“路径”,下同。

<!--export_paths.xml-->
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <root-path name="root" path="xiaoi/shrq/" />

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

    <cache-path name="cache" path="xiaoi/shrq/cache/." />
<!-- path设置为'.'时代表整个SDCard Environment.getExternalStorageDirectory()目录   -->
    <external-path name="sdcard" path="."/>

    <external-files-path name="external_files2" path="xiaoi/shrq/ext_file/." />
    
    <external-cache-path name="external_cache_path" path="." />
    
	<external-media-path name="ext_media" path="media" />

</paths>

在paths节点下支持以下几个子节点:

2.1、< root-path/> 配置设备的根目录

配置了这个节点就代表根目录/xiaoi/shrq/对应的目录及其子目录被公开暴露出去了,即FileProvider需要给根目录/xiaoi/shrq/对应的目录及其子目录下的文件创建对应的URI,下同。

2.2、< files-path/> 配置Context.getFilesDir()

配置了这个节点就代表context.getFilesDir()/xiaoi/shrq/files/.对应的目录及其子目录被公开暴露出去了。

2.3、< cache-path/> 配置Context.getCacheDir()

配置了这个节点就代表context.getCacheDir()/xiaoi/shrq/cache/.对应的目录及其子目录被公开暴露出去了。

2.4、< external-files-path/> 配置Context.getExternalFilesDirs()

配置了这个节点就代表context.getExternalFilesDirs()/xiaoi/shrq/ext_file/.对应的目录及其子目录被公开暴露出去了。

2.5、< external-path/> 配置Environment.getExternalStorageDirectory()

配置了这个节点就代表Environment.getExternalStorageDirectory()+ /path值/对应的目录及其子目录被公开暴露出去了。

2.6、< external-cache-path/> 配置getExternalCacheDir()

配置了这个节点就代表context.getExternalCacheDir() + /path值/对应的目录及其子目录被公开暴露出去了。

注意external-cache-path在support-v4:24.0.0时并未支持,直到support-v4:25.0.0才支持。

2.7、< external-media-path/> 配置Context.getExternalMediaDirs()

配置了这个节点就代表context.getExternalMediaDirs() + /path值/对应的目录及其子目录被公开暴露出去了。

注意:external-media-path在api 21及以上才生效

3、创建content://类型的URI

URI(Universal Resource Identifier)通用资源标志符代表要操作的数据,Android上每种资源、图像、视频片段等都可以用URI来表示,从广义上来说URI包括URL,URI由三大部分组成scheme、authority 和path(其中authority又分为host和port)固定格式如下scheme://host:port/path,如果按照上面的配置那么对应的URI就如下图所示:在这里插入图片描述
在这里插入图片描述

要创建content://类型的Uri需要传入一个已经存在的File对象,如果不存在需要自己new一个对象

private void createFile(String videoName) {
    File parentFile = new File(Environment.getExternalStorageDirectory() + CameraHelper.AVATAR_VIDEO);
    if (!parentFile.exists()) {
        parentFile.mkdirs();
    }
    mVideo = new File(parentFile.getPath(),  videoName+".mp4");
    if(mVideo.exists()){
        mVideo.delete();
    }
    try {
        mVideo.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

获取Uri

/**
* 传入的authority 就是在清单中配置的值
*/
 Uri videoUri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", mVideo);

4、给Uri授予临时权限

  • FLAG_GRANT_READ_URI_PERMISSION——表示读取权限;
  • FLAG_GRANT_WRITE_URI_PERMISSION——表示写入权限。
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);  

可以同时或单独使用这两个权限,根据你自己的项目需求而定。

5、向另一个App提供内容URI

使用Intent传递URI,以打开系统自带的录像功能为例:

private void openSysRecording() {

    Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
    ////在这里的QUALITY参数,值为两个,一个是0,一个是1,代表录制视频的清晰程度,0最不清楚,1最清楚,使用0,录制1分钟大概内存是几兆
    intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);
    //限制时长 ,参数8代表8秒,可以根据需求自己调,最高应该是2个小时,当在这里设置时长之后,录制到达时间,系统会自动保存视频,停止录制
    intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 8);
    intent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, 1024 * 1024 * 100);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    Uri videoUri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", mVideo);
    //在有些版本上若指定MediaStore.EXTRA_OUTPUT的uri值在onActivityResult时data.getData()值就为null,具体看源码的实现
    intent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri);
    //在这里有录制完成之后的操作,系统会默认把视频放到照片的文件夹中
    startActivityForResult(intent, REQ_CODE_RECORD);
}

处理onActivityResult

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
                if(resultCode==RESULT_OK){
            if (requestCode == REQ_CODE_RECORD && data != null) {
                Uri uri = data.getData();
                String path = getRealVideoPath(this, uri);
                if (TextUtils.isEmpty(path)) {
                    return;
                }
                FileInputStream fis = null;
                FileOutputStream fos = null;
                String fileName = mVideo.getPath();
                try {
                    fis = new FileInputStream(path.substring(1));
                    // 创建文件夹
                    fos = new FileOutputStream(fileName);
                    byte[] buffer = new byte[1024];
                    int len = 0;
                    while ((len = fis.read(buffer)) != -1) {
                        fos.write(buffer, 0, len);
                    }
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

从已有的Uri 解析出对应的path:

    /**
     * 从Uri 解析出对应的path
     * @param context
     * @param uri
     * @return
     */
    public static String getRealVideoPath(final Context context, final Uri uri) {
        if (null == uri) {
            return null;
        }
        final String scheme = uri.getScheme();
        String data = null;
        if (scheme == null) {
            data = uri.getPath();
        } else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
            data = uri.getPath();
        } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
            Cursor cursor = context.getContentResolver().query(uri, new String[]{MediaStore.Video.VideoColumns.DATA}, null, null, null);
            if (null != cursor) {
                if (cursor.moveToFirst()) {
                    int index = cursor.getColumnIndex(MediaStore.Video.VideoColumns.DATA);
                    if (index > -1) {
                        data = cursor.getString(index);
                    }
                }
                cursor.close();
            }
        }
        return data;
    }

PS、一些来自网络相关的工具类

public class FileProvider7 {

    public static Uri getUriForFile(Context context, File file) {
        Uri fileUri = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            fileUri = getUriForFile24(context, file);
        } else {
            fileUri = Uri.fromFile(file);
        }
        return fileUri;
    }

    private static Uri getUriForFile24(Context context, File file) {
        Uri fileUri = android.support.v4.content.FileProvider.getUriForFile(context,
                context.getPackageName() + ".fileprovider",
                file);
        return fileUri;
    }


    public static void setIntentDataAndType(Context context,
                                            Intent intent,
                                            String type,
                                            File file,
                                            boolean writeAble) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.setDataAndType(getUriForFile(context, file), type);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            if (writeAble) {
                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            }
        } else {
            intent.setDataAndType(Uri.fromFile(file), type);
            chmod("777", file.getAbsolutePath());//apk放在cache文件中,需要获取读写权限
        }
    }

    public static   void chmod(String permission, String path) {
        try {
            String command = "chmod " + permission + " " + path;
            Runtime runtime = Runtime.getRuntime();
            runtime.exec(command);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void setIntentData(Context context,
                                     Intent intent,
                                     File file,
                                     boolean writeAble) {
        if (Build.VERSION.SDK_INT >= 24) {
            intent.setData(getUriForFile(context, file));
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            if (writeAble) {
                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            }
        } else {
            intent.setData(Uri.fromFile(file));
        }
    }

    public static void grantPermissions(Context context, Intent intent, Uri uri, boolean writeAble) {

        int flag = Intent.FLAG_GRANT_READ_URI_PERMISSION;
        if (writeAble) {
            flag |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
        }
        intent.addFlags(flag);
        List<ResolveInfo> resInfoList = context.getPackageManager()
                .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
        for (ResolveInfo resolveInfo : resInfoList) {
            String packageName = resolveInfo.activityInfo.packageName;
            context.grantUriPermission(packageName, uri, flag);
        }
    }
}

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/102569375