从 Android 7.0 开始,应用私有目录的访问权限被做限制。我们不能够再简单地通过 file:// URI 访问其他应用的私有目录文件或者让其他应用访问自己的私有目录文件。
而且从7.0开始,当我们在应用中使用包含 file:// URI 的 Intent 离开自己的应用时,程序会发生故障。
开发中,如果我们在使用 file:// URI 时忽视了这两条规定,将导致用户在 7.0 及更高版本系统的设备中使用到相关功能时,出现 FileUriExposedException 异常,导致应用出现崩溃闪退问题。而这两个过程的替代解决方案便是使用 FileProvider。
最近写的代码,用到了调用系统的相机和系统的裁剪功能,在7.0以下使用是正常的,当遇到7.0的手机时,选择照片裁剪这一步程序直接就奔溃了。
下面看一下怎么使用拍照和剪裁功能
使用:
1.注册FileFrovider,生成共享目录
首先在自己的清单文件中注册:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${app_package_name}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
provider>
authorities由两部分组成:第一部分是build.gradle 文件中的 applicationId 值,第二部分是自定义的名称,两者组成Uri 字符串
android:resource="@xml/file_paths"
这一步是添加共享目录,具体的目录由自己的需求决定。
<paths>
<files-path
name="root"
path="/" />
<cache-path
name="cache"
path="/" />
<external-path
name="path"
path="/" />
<external-path
name="ext_root"
path="pictures/" />
<external-files-path
name="ext_pub"
path="/" />
<external-cache-path
name="ext_cache"
path="/" />
paths>
2.生成Content URI
在 Android 7.0 出现之前,我们通常使用 Uri.fromFile() 方法生成一个 File URI。这里,我们需要使用 FileProvider 类提供的公有静态方法 getUriForFile 生成 Content URI。比如:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", file);
} else {
uri = Uri.fromFile(file);
}
3. 授予 Content URI 访问权限
生成 Content URI 对象后,需要对其授权访问权限。授权方式有两种:
第一种方式,使用 Context 提供的 grantUriPermission(package, Uri, mode_flags) 方法向其他应用授权访问 URI 对象。三个参数分别表示授权访问 URI 对象的其他应用包名,授权访问的 Uri 对象,和授权类型。其中,授权类型为 Intent 类提供的读写类型常量:
FLAG_GRANT_READ_URI_PERMISSION
FLAG_GRANT_WRITE_URI_PERMISSION
或者二者同时授权。这种形式的授权方式,权限有效期截止至发生设备重启或者手动调用 revokeUriPermission() 方法撤销授权时。
第二种方式,配合 Intent 使用。通过 setData() 方法向 intent 对象添加 Content URI。然后使用 setFlags() 或者 addFlags() 方法设置读写权限,可选常量值同上。这种形式的授权方式,权限有效期截止至其它应用所处的堆栈销毁,并且一旦授权给某一个组件后,该应用的其它组件拥有相同的访问权限。
具体使用:
调用系统拍照:
Intent intentCamera = new Intent();
intentCamera.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", file);
List resInfoList = context.getPackageManager().queryIntentActivities(intentCamera, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
} else {
uri = Uri.fromFile(file);
}
intentCamera.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intentCamera, REQUEST_CODE_TAKE_CAMERA);
打开相册:
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_CODE_IMAGE_FROM_ALBUM);
由于我在调用拍照或者选取相册之后,都使用了照片的剪裁:
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//就是这一步出错了
uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", cropFile);
} else {
uri = Uri.fromFile(cropFile);
}
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", getResources().getDimensionPixelSize(R.dimen.circle_image_view_size));
intent.putExtra("outputY", getResources().getDimensionPixelSize(R.dimen.circle_image_view_size));
intent.putExtra("return-data", false);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(intent, REQUEST_CODE_CROP_IMAGE);
Android7.0以后,这样做会使程序奔溃:
uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", cropFile);
应该这样做:
uri = getImageContentUri(cropFile.getAbsolutePath());
private Uri getImageContentUri(String path){
Cursor cursor = getContext().getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media._ID},
MediaStore.Images.Media.DATA + "=? ",
new String[]{path}, null);
if (cursor != null && cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));
Uri baseUri = Uri.parse("content://media/external/images/media");
return Uri.withAppendedPath(baseUri, ""+id);
}else {
ContentValues contentValues = new ContentValues(1);
contentValues.put(MediaStore.Images.Media.DATA, path);
return getContext().getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
}
}
这样做就没问题了。