android 7.0 使用FileProvider在应用间共享文件(相机适配)

好久没记录我的工作了,事太多了,今天刚好有点时间就捋一捋上一个项目中做的有关7.0的相机适配吧,主要是FileProvider的使用,当时主要是因为7.0手机拍照功能引出的我对FileProvider的深入,下面就记录下我是怎么适配的吧

一、FileProvider概述

FileProvider是我们对所有实现了IFileProvider接口的所有类型以及对应对象的统称。
总的来说,以FileProvider为核心的文件系统在设计上看是非常简单的。除了FileProvider,文件系统还涉及到其他一些对象,比如DirectoryContents、FileInfo和ChangeToken。这些对象都具有对应的接口定义,下图所示的UML展示了涉及的这些接口以及它们之间的关系

android 7.0 使用FileProvider在应用间共享文件(相机适配)_第1张图片

废话就不多说了,进入正题。
我们经常会遇到项目中传递file://类似格式的uri(特别是拍照功能),但是在android 7.0以上系统中官方是这么写的
《在官方7.0的以上的系统中,尝试传递 file://URI可能会触发FileUriExposedException》
FileProvider是ContentProvider的一个子类
通常的相机调用是这样的(6.0以上系统注意需要动态申请权限了)

 private static final int REQUEST_CODE_TAKE_PHOTO = 0;
    private String mCurrentPhotoPath;
     public void takePhoto(View view) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null{
            String filename = new SimpleDateFormat("yyyy-MMdd-HHmmss", Locale.CHINA)
                    .format(new Date()) + ".png";
            File file = new File(Environment.getExternalStorageDirectory(), filename);
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
            startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_TAKE_PHOTO) {
            mIvPhoto.setImageBitmap(BitmapFactory.decodeFile(mCurrentPhotoPath));
        }
    }

以上代码也是我最开始操作相机的,但是在android7.0系统上运行之后直接闪退了,查看crash日志发现android.os.FileUriExposedException,之后翻阅资料发现原来android7.0之后要在应用间共享文件,需要发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类(官方给的解决方案)
具体就是之前的file:///Uri不给用,必须换个Uri为content://来替代了

1、在AndroidManifest中先声明provider

前面也说了FileProvider是ContentProvider子类也就是说是4大组件之一,那在AndroidManifest中声明就不足为奇了

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

2、xml资源文件


<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <root-path name="root" path="" />
    <files-path name="files" path="" />
    <cache-path name="cache" path="" />
    <external-path name="external" path="" />
    <external-files-path name="name" path="path" />
    <external-cache-path name="name" path="path" />
paths>

在paths节点内部支持以下几个子节点,分别为

  • 代表设备的根目录new File(“/”);
  • 代表context.getFilesDir()
  • 代表context.getCacheDir()
  • 代表Environment.getExternalStorageDirectory()
  • 代表context.getExternalFilesDirs()
  • 代表getExternalCacheDirs()

按自己需要填写path值说到这个文件目录,我突然想起来之前看过的一张图完全总结了看起来一目了然非常有用,还是贴出来吧

/**
     * |   ($rootDir)
     * +- /data                    -> Environment.getDataDirectory()
     * |   |
     * |   |   ($appDataDir)
     * |   +- data/$packageName
     * |       |
     * |       |   ($filesDir)
     * |       +- files            -> Context.getFilesDir() / Context.getFileStreamPath("")
     * |       |      |
     * |       |      +- file1     -> Context.getFileStreamPath("file1")
     * |       |
     * |       |   ($cacheDir)
     * |       +- cache            -> Context.getCacheDir()
     * |       |
     * |       +- app_$name        ->(Context.getDir(String name, int mode)
     * |
     * |   ($rootDir)
     * +- /storage/sdcard0         -> Environment.getExternalStorageDirectory()/ Environment.getExternalStoragePublicDirectory("")
     * |                 |
     * |                 +- dir1   -> Environment.getExternalStoragePublicDirectory("dir1")
     * |                 |
     * |                 |   ($appDataDir)
     * |                 +- Andorid/data/$packageName
     * |                                         |
     * |                                         | ($filesDir)
     * |                                         +- files                  -> Context.getExternalFilesDir("")
     * |                                         |    |
     * |                                         |    +- file1             -> Context.getExternalFilesDir("file1")
     * |                                         |    +- Music             -> Context.getExternalFilesDir(Environment.Music);
     * |                                         |    +- Picture           -> Context.getExternalFilesDir(Environment.Picture);
     * |                                         |    +- ...               -> Context.getExternalFilesDir(String type)
     * |                                         |
     * |                                         |  ($cacheDir)
     * |                                         +- cache                  -> Context.getExternalCacheDir()
     * |                                         |
     * |                                         +- ???
     * 

*

* 1. 其中$appDataDir中的数据,在app卸载之后,会被系统删除。 *

* 2. $appDataDir下的$cacheDir: * Context.getCacheDir():机身内存不足时,文件会被删除 * Context.getExternalCacheDir():空间不足时,文件不会实时被删除,可能返回空对象,Context.getExternalFilesDir("")亦同 *

* 3. 内部存储中的$appDataDir是安全的,只有本应用可访问 * 外部存储中的$appDataDir其他应用也可访问,但是$filesDir中的媒体文件,不会被当做媒体扫描出来,加到媒体库中。 *

* 4. 在内部存储中:通过 Context.getDir(String name, int mode) 可获取和 $filesDir / $cacheDir 同级的目录 * 命名规则:app_ + name,通过Mode控制目录是私有还是共享 *

* * Context.getDir("dir1", MODE_PRIVATE): * Context.getDir: /data/data/$packageName/app_dir1 * */

3、FileProvider API的使用
通过FileProvider把file转化为content://uri

public void takePhotoNoCompress(View view) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                    .format(new Date()) + ".png";
            File file = new File(Environment.getExternalStorageDirectory(), filename);
            Uri fileUri = FileProvider.getUriForFile(this, "com.army.mvpsimple.fileprovider", file);  // 核心代码 
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
            startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
        }
    }

现在7.0上运行就没问题了,没有多少代码其实,这里可以自打印下生成的uri验证是不是content:// file路径

真正在一个项目使用看着好像这么解决了当前遇到的问题,但是我使用4.+的系统一试问题来了,有crash,这是什么鬼,一查是权限问题导致的这就很尴尬了,contentprovider的export设置的也是false意味着别的应用不能访问你所以就会导致导致Permission Denial但是我们看FileProvider源码已经确定了exported必须是false,grantUriPermissions必须是true

@Override
public void attachInfo(Context context, ProviderInfo info) {
    super.attachInfo(context, info);

    // Sanity check our security
    if (info.exported) {
        throw new SecurityException("Provider must not be exported");
    }
    if (!info.grantUriPermissions) {
        throw new SecurityException("Provider must grant uri permissions");
    }

    mStrategy = getPathStrategy(context, info.authority);
}

既然这样我们就只有直接判断版本了

if (Build.VERSION.SDK_INT >= 24) {
    fileUri = FileProvider.getUriForFile(this, "com.army.mvpsimple.fileprovider", file);
} else {
    fileUri = Uri.fromFile(file);
}

这里适配android 7.0 拍照的问题就解决了

总结:使用content://替代file://,主要需要FileProvider的支持,而因为FileProvider是ContentProvider的子类,所以需要在AndroidManifest.xml中注册;而又因为需要对真实的filepath进行映射,所以需要编写一个xml文档,用于描述可使用的文件夹目录,以及通过name去映射该文件夹目录。

你可能感兴趣的:(Android)