随着Android版本越来越高,Android对隐私的保护力度也越来越大。比如:Android6.0引入的动态权限控制(Runtime Permissions),Android7.0又引入“私有目录被限制访问”,“StrictMode API 政策”。这些更改在为用户带来更加安全的操作系统的同时也为开发者带来了一些新的任务。如何让你的APP能够适应这些改变而不是cash,是摆在每一位Android开发者身上的责任。

1.

2.  "StrictMode API 政策" 是指禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常。

虽然政策很严格,但谁说有守门员就不让进球的?使用FileProvider可以解决这个问题,FileProvider是ContentProvider的一个特殊子类, 它的原理是通过创建一个 content:// Uri  来代替file:// Uri ,这样在N上跑的应用程序就不会因为" StrictMode API 政策" 而crash,从而实现 应用程序间的文件安全共享。

FileProvider的使用大致步骤如下:

【第一步】 :在AndroidManifest.xml清单文件中注册provider,因为provider也是Android四大组件之一,可以简单把它理解为向外提供数据的组件。

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

exported:要求必须为false,为true则会报安全异常。grantUriPermissions:true,表示授予 URI 临时访问权限。authorities 组件标识,一般以包名开头。

【第二步】:指定共享的目录

上面配置文件中 android:resource="@xml/file_paths" 指的是当前组件引用 res/xml/file_paths.xml 这个文件。我们需要在资源(res)目录下创建一个xml目录,然后创建一个名为“file_paths”(名字可以随便起,只要和在manifest注册的provider所引用的resource保持一致即可)的资源文件,内容如下:

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

【分析】同之前分析一样,目的是使用content:// Uri代替 file:// Uri,那么,content:// Uri 的Uri如何定义呢?总不能使用文件路径吧,那不是白忙活么~ 所以,需要一个虚拟的路径对真实文件路径进行映射,通过<path/>的 path属性确定可访问的目录,name属性来映射真实的文件路径.

上 述代码中path=""是有特殊意义的,它代表根目录,也就是说此应用可以向其它的应用共享外部存储区根目录及其子目录下任何一个文件了,如果你将path设为path="pictures",那么它代表着只能共享外部存储区根目录下的pictures目录(eg:/storage/emulated/0/pictures),这样你向其它应用分享pictures目录范围之外的文件是不行的,所以path=""表述的范围更大.

<paths/>标签中的元素可以是以下任意组合:

•<files-path/>代表应用在内部存储区(Internal Storage)的私有files根目录:

    context.getFilesDir()

-->

•<cache-path/>代表应用在内部存储区(Internal Storage)的私有cache根目录:

    context.getCacheDir()

-->

•<external-path/>代表外部存储区(External Storage)的根目录:

    Environment.getExternalStorageDirectory()

-->

•<external-files-path/>代表应用在外部存储区(External Storage)的私有files根目录:

    context.getExternalFilesDir(null)

-->

    context.getExternalFilesDir(String type)

-->

•<external-cache-path/>代表应用在外部存储区(External Storage)的私有cache根目录:

context.getExternalCacheDir()

-->

NOTE: 以上的路径为 MEIZU MX5(API22) 真机测试结果,测试程序并没有应用FileProvider;而如果是使用FileProvider分享代表一个文件的content://Uri (隐含 API≥24❶情况,否则直接用file://Uri,何必来绕这一圈!),结合 FileProvider API:FileProvider.getUriForFile(Context context, String authority , File file ) 分析,content://Uri的形式为:

[Internal-cache&files] content:// authority / name / filename [结合第三步注释]

[External-private-cache] content:// authority / name /Android/data/包名/cache/filename

[External-private-files] content:// authority / name / Android/data/包名/files/"type"/ filename

[External-public]  content:// authority / name / "type" / filename

"type"因参数不同而不同,比如 "Environment.DIRECTORY_PICTURES" 对应 Pictures 目录.

还是说一下 API<24的情况:

API<24 ❷情况 file:// -->

【第三步】:使用FileProvider

上述准备工作做完之后,现在我们就可以使用FileProvider了。

public void takePhoto(View view) {
 
  
"output_image.jpg" 为 filename
 
  
    File outputImage = new File(getExternalCacheDir(), " 
   output_image.jpg 
   "); 
 
  
    try {
 
  
        if (outputImage.exists()) {
 
  
            outputImage.delete();
 
  
        }
 
  
        outputImage.createNewFile();
 
  
 
   
 
  
    } catch (IOException e) {
 
  
        e.printStackTrace();
 
  
    }
 
  
 
   
 
  
    if (Build.VERSION.SDK_INT < 24) {
 
  
            // 如果运行设备系统版本低于7.0 , 可以直接使用file://Uri,直接将File对象转换成Uri对象
 
  
        imageUri = Uri.fromFile(outputImage);
 
  
    } else {
 
  
            // 否则将File对象转换成一个封装过的Uri对象。核心代码,将file:// Uri 转换成 content:// Uri
 
  
        imageUri = FileProvider.getUriForFile(MainActivity.this, "com.example.four.cameraalbumtest.fileprovider", outputImage);
 
  
    }
 
  
    openCamera();
 
  
}
 
  
 
   
 
  
public void openCamera() {
 
  
    
   // 启动相机程序
 
  
    Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
 
  
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 
  
    intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
 
  
    startActivityForResult(intent, 1);
 
  
}

【常见使用场景】

  1. 调用照相机,指定照片存储路径。
  2. 调用系统安装器,传递apk文件。

调用照相机:

/**
 
   
* @param activity 当前activity
 
   
* @param authority FileProvider对应的authority
 
   
* @param file 拍照后照片存储的文件
 
   
* @param requestCode 调用系统相机请求码
 
   
*/
 
   
public static void takePicture(Activity activity, String authority, File file, int requestCode) {
 
   
    Intent intent = new Intent();
 
   
    Uri imageUri;
 
   
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 
   
        imageUri = FileProvider.getUriForFile(activity, authority, file);
 
   
// 赋予临时权限的常用方法
 
   
    } else {
 
   
        imageUri = Uri.fromFile(file);
 
   
    }
 
   
    intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
 
   
    intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
 
   
    activity.startActivityForResult(intent, requestCode);
 
   
}

调用安装器:

/**
 
   
* 调用系统安装器安装apk
 
   
*
 
   
* @param context 上下文
 
   
* @param authority FileProvider对应的authority
 
   
* @param file apk文件
 
   
*/
 
   
public static void installApk(Context context, String authority, File file) {
 
   
    Intent intent = new Intent(Intent.ACTION_VIEW);
 
   
    Uri apkUri;
 
   
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 
   
apkUri
 
   
        
     intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 赋予临时权限的常用方法
 
   
    } else {
 
   
apkUri = Uri.fromFile(file);
 
   
    }
 
   
apkUri, INSTALL_TYPE);
 
   
    context.startActivity(intent);
 
   
}