Android7.0适配:关于Android 7.0 在应用间共享文件的适配

前言

随安卓版越来越高,对隐私保护力度亦越来越大。从Android6.0动态权限控制(Runtime Permissions)到Android7.0私有目录限访、StrictMode API政策等的更改,这些更改在为用户带来更加安全的操作系统的同时也为开发者做应用适配带来了一些新的任务,所以我们有必要对其进行了解,这也是我整理这篇文章的原因。

错误描述

在Android7.0系统上。Android 框架强制运行了 StrictMode API 政策禁止向你的应用外公开 file:// URI。
给其他应用传递 file:// URI 类型的Uri,可能会导致接受者无法访问该路径。 因此,如果在Android7.0及以上系统尝试传递 file:// URI 就会触发 FileUriExposedException异常,如调用系统相机拍照或者裁剪照片,调用系统的安装程序执行apk的安装等等操作,如果不适配在Android 7.0 及以上系统就会出现应用崩溃的现象。

关于Android 7.0应用间共享文件的适配方案

在Android 7.0 之前我们通过File生成Uri的代码通常是这样的:

File picFile = new File(pathString);
Uri picUri = Uri.fromFile(picFile);

这样生成的Uri的路径格式为file://xxx。这种Uri是无法在应用之间共享的,我们需要生成content://xxx类型的Uri,要获取这种类型的Uri最简单方式是使用 FileProvider类。

FileProvider类简介

FileProvider是一个特殊的ContentProvider子类,它通过为文件创建一个Content:/ / Uri而不是file:/ / Uri,从而促进与应用程序关联的文件的安全共享。

FileProvider的使用

FileProvider使用大概分为以下几个步骤:
  1,在manifest清单文件里注冊provider
  2,res/xml中定义对外暴露的文件夹路径
  3,生成content://类型的Uri
  4,给Uri授予临时权限
  5,使用Intent传递Uri

1,在manifest清单文件里注冊provider

<manifest>
  ...
  <application>
    ...
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.demo.fileprovider"
        android:exported="false"  // 必须为false
        android:grantUriPermissions="true">   // 必须为true才具有临时共享权限
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
    ...
  </application>
</manifest>

exported:要求必须为false,为true则会报安全异常。grantUriPermissions:true,设置为true你才能获取临时共享权限。
android:name:provider你可以使用v4包提供的FileProvider,或者自定义的,只需要在name申明就好了,一般使用系统的就足够了。
android:authorities:类似schema,命名空间之类,可以自定义,后面会用到。
2,res/xml中定义对外暴露的文件夹路径
为了指定共享的文件夹我们须要在资源(res)文件夹下创建一个xml文件夹,然后创建一个名为“file_paths”(名字能够随便起,但是要和在manifest注冊的provider所引用的resource保持一致)的资源文件。内容例如以下:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="external_files"
        path="" />
</paths>

name:一个引用字符串,用于给访问路径起个名字。
path:文件夹“相对路径”,完整路径取决于当前的标签类型。

上述代码中path="",是有特殊意义的,它代表根文件夹。也就是说你能够向其它的应用共享根文件夹及其子文件夹下任何一个文件了,假设你将path设为path=”pictures”,
那么它代表着根文件夹下的pictures文件夹(eg:/storage/emulated/0/pictures),假设你向其它应用分享pictures文件夹范围之外的文件是不行的。

paths这个元素内可以包含以下一个或多个,具体如下:

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

物理路径相当于Context.getFilesDir() + /path/。

<cache-path name="name" path="path" />

物理路径相当于Context.getCacheDir() + /path/。

<external-path name="name" path="path" />

物理路径相当于Environment.getExternalStorageDirectory() + /path/。

<external-files-path name="name" path="path" />

物理路径相当于Context.getExternalFilesDir(String) + /path/。

<external-cache-path name="name" path="path" />

物理路径相当于Context.getExternalCacheDir() + /path/。
3,生成content://类型的Uri

File apkFile=new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),"app-release.apk");
String authorities = "com.example.aa.fileProvider";
Uri uriForFile = FileProvider.getUriForFile(this, authorities, apkFile);

上面注意authorities应该与清单文件中配置的authorities属性值保持一致。
4,给Uri授予临时权限

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
               | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

FLAG_GRANT_READ_URI_PERMISSION:表示读取权限;
FLAG_GRANT_WRITE_URI_PERMISSION:表示写入权限。
5,使用Intent传递Uri
如在app版本更新适配7.0时:

install.setDataAndType(uriForFile,"application/vnd.android.package-archive");
startActivity(install);

app版本更新适配7.0时,调用系统安装应用完成apk安装的代码如下:

要在当前应用和系统安装应用之间共享下载好的apk文件,所以要适配。

private void installAPK() {
     Intent install = new Intent(Intent.ACTION_VIEW);
     // 找到下载好的apk
     File apkFile=new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),"new.apk");
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
               String authorities = "com.example.aa.fileProvider";
               Uri uriForFile = FileProvider.getUriForFile(this, authorities, apkFile);
               install.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);               
               install.setDataAndType(uriForFile,"application/vnd.android.package-archive");
          }else {
                install.setDataAndType(Uri.fromFile(apkFile), "applicati on/vnd.android.package-archive");
          }
      install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
      startActivity(install);
    }

拍照适配

String cachePath = getApplicationContext().getExternalCacheDir().getPath();
File picFile = new File(cachePath, "test.jpg");
Uri picUri = Uri.fromFile(picFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, picUri);
startActivityForResult(intent, 100);

这是常见的打开系统相机拍照的代码,拍照成功后,照片会存储在picFile文件中。
这段代码在Android 7.0之前是没有任何问题,但是如果你尝试在7.0的系统上运行,会抛出FileUriExposedException异常,为了适配7.0我们修改为如下所示:

File imagePath = new File(Context.getFilesDir(), "images");
if (!imagePath.exists()){imagePath.mkdirs();}
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), 
                 "com.mydomain.fileprovider", newFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
// 授予目录临时共享权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
               | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, 100); 

裁剪适配

File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
if (!file.getParentFile().exists())file.getParentFile().mkdirs();
Uri outputUri = Uri.fromFile(file);
Uri imageUri=Uri.fromFile(new File("/storage/emulated/0/temp/1474960080319.jpg"));
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(imageUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("scale", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true); // no face detection
startActivityForResult(intent,1008);

和拍照一样。上述代码在Android7.0上相同会引起android.os.FileUriExposedException异常,解决的方法就是上文说说的使用FileProvider,将上述代码改为例如以下就可以:

File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
if (!file.getParentFile().exists())file.getParentFile().mkdirs();
Uri outputUri = FileProvider.getUriForFile(context, "com.jph.takephoto.fileprovider",file);
Uri imageUri=FileProvider.getUriForFile(context, "com.jph.takephoto.fileprovider", new File("/storage/emulated/0/temp/1474960080319.jpg");//通过FileProvider创建一个content类型的Uri
Intent intent = new Intent("com.android.camera.action.CROP");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(imageUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("scale", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true); // no face detection
startActivityForResult(intent,1008);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智玲君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值