参考链接:
https://developer.android.com/reference/android/support/v4/content/FileProvider
https://blog.csdn.net/lmj623565791/article/details/72859156
http://yifeng.studio/2017/05/03/android-7-0-compat-fileprovider/ FileProvider 的使用
FileProvider 实际上是 ContentProvider 的一个子类,它的作用也比较明显了,file://Uri 不给用,那么换个 Uri 为 content://
来替代。
在官方7.0的以上的系统中,尝试传递 file://uri/ 可能会触发
FileUriExposedException
private static final int REQUEST_CODE_TAKE_PHOTO = 0x110;
private String mCurrentPhotoPath;
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);
mCurrentPhotoPath = file.getAbsolutePath();
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));
}
// else tip?
}
此时如果我们使用 Android 7.0 或者以上的原生系统,再次运行一下,你会发现应用直接停止运行,抛出了 android.os.FileUriExposedException
:
Caused by: android.os.FileUriExposedException:
file:///storage/emulated/0/20170601-030254.png exposed beyond app through ClipData.Item.getUri()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1932)
at android.net.Uri.checkFileUriExposed(Uri.java:2348)
官网解释:
为了提高私有目录的安全性,防止应用信息的泄漏,从 Android 7.0 开始,应用私有目录的访问权限被做限制。具体表现为,开发人员不能够再简单地通过 file:// URI 访问其他应用的私有目录文件或者让其他应用访问自己的私有目录文件。对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。
官网解决方案:
要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。
打开一个4.4的模拟器,运行上述代码,你会发现又Crash啦,抛出了:Permission Denial~
Caused by: java.lang.SecurityException: Permission Denial: opening provider android.support.v4.content.FileProvider from ProcessRecord{52b029b8 1670:com.android.camera/u0a36} (pid=1670, uid=10036) that is not exported from uid 10052
at android.os.Parcel.readException(Parcel.java:1465) at android.os.Parcel.readException(Parcel.java:1419)
at android.app.ActivityManagerProxy.getContentProvider(ActivityManagerNative.java:2848)
at android.app.ActivityThread.acquireProvider(ActivityThread.java:4399)
因为低版本的系统,仅仅是把这个当成一个普通的 Provider 在使用,而我们没有授权,contentprovider 的 export 设置的也是 false;导致 Permission Denial。那么,我们是否可以将 export 设置为 true 呢?很遗憾是不能的。在 FileProvider 的内部:
@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);
}
确定了 exported 必须是 false,grantUriPermissions 必须是 true,所以唯一的办法就是授权了。context 提供了两个方法:
- grantUriPermission(String toPackage, Uri uri, int modeFlags)
- revokeUriPermission(Uri uri, int modeFlags);
可以看到 grantUriPermission 需要传递一个包名,就是你给哪个应用授权,但是很多时候,比如分享,我们并不知道最终用户会选择哪个 app,所以我们可以这样:
List<ResolveInfo> resInfoList = context.getPackageManager()
.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, uri, flag);
}
常见使用场景:
1、自动安装文件
2、调用系统拍照
3、调用系统裁剪
简单总结:
总结下,使用 content:// 替代 file://,主要需要 FileProvider 的支持,而因为 FileProvider 是 ContentProvider 的子类,所以需要在 AndroidManifest.xml 中注册;而又因为需要对真实的 filepath 进行映射,所以需要编写一个 xml 文档,用于描述可使用的文件夹目录,以及通过 name 去映射该文件夹目录。
对于权限,有两种方式:
- 方式一:Intent.addFlags,该方式主要用于针对 intent.setData,setDataAndType 以及 setClipData 相关方式传递 uri 的
- 方式二:grantUriPermission 来进行授权
/**
* 因为低版本的系统,仅仅是把 FileProvider 当成一个普通的 Provider 在使用,而我们没有授权,
* FileProvider 的 export 设置的也是 false,导致 Permission Denial,出现 crash。
* 所以我们可以配合 Intent 使用:通过 setData() 方法向 intent 对象添加我们的 Content URI,
* 然后使用 setFlags() 或者 addFlags() 方法设置读/写权限。
* 拥有授予权限的 Content URI 后,便可以通过 startActivity() 或者 setResult() 方法启动其他应用并传递授权过的 Content URI 数据
*/
public static Intent addReadWriteFlagNougat(Intent intent) {
if (SystemUtil.hasNougat()) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
return intent;
}