Android7.0 应用间共享文件 FileProvider

  项目中要求拍照后在相册图库中并不能被用户可见(防止用户在app以外修改), 然后将照片上传到服务器,由于服务器接收到照片需要预览,所以还是需要以.jpg的格式保存,(PS:本来想直接以一个文件形式保存,然后服务器自己修改个后缀名就可以了哈),想了想,那不就是启动系统相机的时候指定好存储路径在内部data/data/包名下即可,(ps: 这种不可见不包括root的用户啊)于是就有了以下这段代码:

btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //存储目录为 date/date/com.ypf.read_write/app_YPF/
                startCarmera(MainActivity.this,getDir("YPF", Context.MODE_APPEND).toString(),"test.jpg");
            }
        });

private void startCarmera(Activity context,String savePath, String photoName) {
        Uri imageUri;
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        File outputImage = new File(savePath,photoName);
        try {
            if (outputImage.exists())
            {
                outputImage.delete();
            }
            outputImage.createNewFile();
        }catch (IOException e){
            e.printStackTrace();
        }
            imageUri = Uri.fromFile(outputImage);
        intent.putExtra(MediaStore.EXTRA_OUTPUT,imageUri);
        startActivityForResult(intent,REQUEST_CODE_IMAGE_CAPTURE);
    }

  乍一看,这代码没毛病,信心满满的跑起程序来,结果出人意外,图片0字节!即我们成功创建了这个文件,但是却没能对其进行写入数据操作。按照我自己的理解,那就是哪怕相机是系统程序,也没有能够对我们包名下的文件进行写入操作,于是我加上了
  

 try {
            Runtime.getRuntime().exec("chmod 777 " + outputImage.getAbsolutePath());
        } catch (IOException e) {
            e.printStackTrace();
        }

在Unix和Linux的各种操作系统下,每个文件(文件夹也被看作是文件)都按读、写、运行设定权限。
读、写、运行三项权限可以用数字表示,就是r=4,w=2,x=1。
其中a,b,c各为一个数字,分别表示User、Group、及Other的权限。
  r=4,w=2,x=1
  若要rwx属性则4+2+1=7;
  若要rw-属性则4+2=6;
  若要r-x属性则4+1=7。
  
  777即代表了User、Group、Other都对其拥有,可读,可写,可运行的权限。
经过测试,这招确实有用,图片能够写入文件咯,但是好景不长,没一天功夫,测试大哥说Android 7.0出现了崩溃现象,一看崩溃日志信息,显示FileUriExposedException。于是查阅的7.0的行为变更,发现以下一段话。

在应用间共享文件
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。

要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件。

原来从Android 7.0 开始,直接使用本地真实路径的Uri被认为是不安全的,会抛出一个FileUriExposedException异常。而FileProvider则是一种特殊的内容提供器,它使用了和内容提供器类似的机制来对数据进行了保护,可以选择性地将封装过的Uri共享给外部,从而提高应用的安全性。
  于是乎,跟着官方文档码了起来,一共五部曲。
 

1 定义一个fileprovider
2 指定可用的文件
3 检索文件的内容URI
4 授予URI的临时权限
5 服务内容URI到另一个应用程序

首先需要再manifest定义一个Provider

<manifest>
    ...
    <application>
        ...
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.mydomain.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            ...
        </provider>
        ...
    </application>
</manifest>

FileProvider需要事先指定好他想要赋予读写权限的路径,所以我们需要新建一个xml目录,创建一个file_paths.xml文件,然后将内容修改成下图:

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

file-path标签相当于Context.getFilesDir().即data/data/包名/files文件夹
cache_path标签相当于 getCacheDir().
external-path标签相当于Environment.getExternalStorageDirectory().
external-files-path相当于Context#getExternalFilesDir(String) Context.getExternalFilesDir(null).
external-cache-path相当于Context.getExternalCacheDir().
其中name属性可以随便填写,path属性的值表示共享的具体路径,设置为空值时,如file-path标签,即表示共享data/data/包名/files文件夹下整个目录
  接下来需要将这个xml关联到manifest的provider标签下。最终代码如下
  

<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/file_paths" />
</provider>

  上面的name属性是固定的,不能随意更改的!
  上面设置完成后,需通过gitUriForFile方法临时授予权限
  

File newFile = new File(Context.getFilesDir(), "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);

  其中第二个参数是provider标签下的authorities的值。
  
  授予权限是临时的,官方有这样一段话。
  Permissions granted in an Intent remain in effect while the stack of the receiving Activity is active. When the stack finishes, the permissions are automatically removed. Permissions granted to one Activity in a client app are automatically extended to other components of that app.
  按照翻译过来的意思和个人的理解,应该是当第一个参数传入的Activity的context,这个activity还在task中则依旧存在,则权限是一直持有的,一旦activity在task中去除了,权限也就收回了。
  最后在获取Uri的时候进行版本判断,如果7.0则使用FileProvider即可,代码就不贴出来了。
  
  开发过程中,发现一部htc的D826d中,发现系统图库中依旧可以看到照片,猜测系统相机拍摄时,在DCIM中将照片备份了一份,于是使用ContentProvider进行了删除,其中图库中的照片,是由封面的的缩略图和点击展开的大图组成。两者由MediaStore.Images.Media._ID=MediaStore.Images.Thumbnails._ID关联,过程如下,拍照后判断最近一条记录的date_added字段,与现在时间进行判断,如果5s内则进行记录删除,并去储存空间里进行文件删除,记得通过绑定关系对缩略图也进行删除哦。大体代码贴下:
  

//删除照片:
                    Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                    ContentResolver mContentResolver = InStorePhotoActivity.this.getContentResolver();
                    //只查询jpeg和png的图片
                    Cursor mCursor = mContentResolver.query(mImageUri, null,
                            MediaStore.Images.Media.MIME_TYPE + "=? or "
                                    + MediaStore.Images.Media.MIME_TYPE + "=?",
                            new String[]{"image/jpeg", "image/png"}, MediaStore.Images.Media.DATE_ADDED);

                    if (mCursor == null) {
                        return;
                    }
                    if (mCursor.moveToLast()) {
                        String path = mCursor.getString(mCursor
                                .getColumnIndex(MediaStore.Images.Media.DATA));
                        String date = mCursor.getString(mCursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED));
                        String id = mCursor.getString(mCursor.getColumnIndex(MediaStore.Images.Media._ID));
                        if ((System.currentTimeMillis() / 1000 - Integer.parseInt(date)) < 6) {
                            FileUtils.delete(path);
                            mContentResolver.delete(mImageUri, MediaStore.Images.Media._ID + " = ?", new String[]{id});

                            Uri mThumbUri = MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI;
                            ContentResolver mThumbResolver = InStorePhotoActivity.this.getContentResolver();
                            Cursor cursorThumb = mThumbResolver.query(mThumbUri, null,
                                    MediaStore.Images.Thumbnails._ID + " = ?", new String[]{id}, null);
                            if (cursorThumb.moveToNext()) {
                                String thumbPath = mCursor.getString(mCursor
                                        .getColumnIndex(MediaStore.Images.Thumbnails.THUMB_DATA));
                                FileUtils.delete(thumbPath);
                                mThumbResolver.delete(mThumbUri, MediaStore.Images.Thumbnails._ID + " = ?", new String[]{id});
                            }
                        }
                    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值