• FileProvider使用详解(拍照、安装APP、共享文件)


    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...(需要梯子)

  • 相关阅读:
    为什么要用设计模式?先看看6大原则(一)
    git版本库的创建和yaf框架环境的部署
    laravel日常小问题
    Session store not set on request.
    phpstudy集成环境安装lavarel
    html中提交表单并实现不跳转页面处理返回值
    document load 与document ready的区别
    定时器优化
    放大镜
    子组件调用父组件的方法并传递数据
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/12970906.html
Copyright © 2020-2023  润新知