Android FileProvider
看这里
Android 7之后, 在应用间 传递 file://
形式的Uri会直接报错: FileUriExposedException.
所以, 网上的很多调用相机拍照获取图片的案例都不能用, 比如这个
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);
}
}
因为使用了 file 格式的Uri, 所以运行会出错.
Android的这个策略是: 禁止我们的应用, 向外部公开 file 格式的 Uril. 所以在应用内部使用 file格式的Uri是可以的, 但是一旦 intent离开我们的应用, 就会出异常.
Android7要求在应用之间共享文件, 必须使用 content格式的Uril, 最简单的是使用 FileProvider 类.
使用方法如下:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.zhy.android7.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
需要注意的是, 最新的 v4 包中已经不包含 FileProvider了, 全部移动到 androidx 中了, 所以应该使用android:name="androidx.core.content.FileProvider"
,
然后编写 file_paths.xml文件
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root" path="" />
<files-path name="files" path="" />
<cache-path name="cache" path="" />
<external-path name="external" path="" />
<external-files-path name="name" path="path" />
<external-cache-path name="name" path="path" />
</paths>
FileProvider会根据这个文件中的内容, 为对应的路径生成content格式的uri.
path节点支持以下子节点
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root" path="" />
<files-path name="files" path="" />
<cache-path name="cache" path="" />
<external-path name="external" path="" />
<external-files-path name="name" path="path" />
<external-cache-path name="name" path="path" />
</paths>
<root-path/> 代表设备的根目录new File("/");
<files-path/> 代表context.getFilesDir()
<cache-path/> 代表context.getCacheDir()
<external-path/> 代表Environment.getExternalStorageDirectory()
<external-files-path>代表context.getExternalFilesDirs()
<external-cache-path>代表getExternalCacheDirs()
每个子节点有两个属性: name 和 path,
name用在生成的uri中, 可以展示给用户.
path是我们的真实的文件路径, 这个不会放在uri中, path必须是一个目录, 不能是单个的文件. 不能通过path来共享单个文件, 也不能指定通配符来共享该目录的一部分文件.
接下来就可以调用相机拍照了
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();
Uri fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
}
这种方法在7.0的手机上没有问题, 但是在4.4上就不行了. 在4.4上别的应用因为权限问题访问不了我们的目录, 这个时候我们需要通过Context的 grantUriPermission来临时授予权限
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);
}
根据Intent查出来的应用都给授权
下面是完整代码
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();
Uri fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
List<ResolveInfo> resInfoList = getPackageManager()
.queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
}
}
其实新老版本的主要差别就是在于Uri的获取方式不对, 所以, 也可以偷懒
Uri fileUri = null;
if (Build.VERSION.SDK_INT >= 24) {
fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
} else {
fileUri = Uri.fromFile(file);
}
但是这样的话, 还是需要临时授权.
每次都要给遍历到的Activity临时授权, 肯定比较麻烦, Android还提供了一个比较方便的方式, 那就是通过Intent的Flag.
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
只要加上这两个flag, 目标App就有临时权限了.
目前来看, 这个 addFlag 生效, 只在 Camera 相关的intent中有效. 这是因为在FrameWork中, 针对几个 Camera 相关的 intent做了特殊处理, 把 OUT_PUT Uri封装到ClipData中, ClipData可以忽略其中Intent或者Uri的本来权限, 只使用外部Intent的权限
if (MediaStore.ACTION_IMAGE_CAPTURE.equals(action)
|| MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(action)
|| MediaStore.ACTION_VIDEO_CAPTURE.equals(action)) {
final Uri output;
try {
output = getParcelableExtra(MediaStore.EXTRA_OUTPUT);
} catch (ClassCastException e) {
return false;
}
if (output != null) {
setClipData(ClipData.newRawUri("", output));
addFlags(FLAG_GRANT_WRITE_URI_PERMISSION|FLAG_GRANT_READ_URI_PERMISSION);
return true;
}
}
尤其要注意的是, addFlag一般用于 setData, setDataAndType和setClipData, 因为 setClipData是5.0才添加的, 所以在5.0以下这种方式没有效果.