您的位置:首页 > 移动开发 > Android开发

Android N 7.0 应用间共享文件(FileProvider)

2017-10-20 20:07 886 查看

Android N 之前的 Uri

常规Uri有两种:

媒体文件的Uri是content://, 表示这是一个数据库数据。去数据库查询正常返回。

其他的文件Uri是file://, 表示这个是一个文件。这个uri是通过Uri.fromFile(File file)方法生成。

Android N 之前,这些uri可以传递到其他应用。

Android N 中共享文件

Android N 系统,Android 框架执行的 StrictMode,API 禁止向您的应用外公开 file://URI

如果一项包含文件 URI 的 Intent 离开您的应用,应用会停止运行,并出现 FileUriExposedException异常。官方文档在Android 7.0 行为变更进行了详细说明

android.os.FileUriExposedException:
file:///storage/emulated/0/Download/appName-2.3.0.apk exposed beyond app through Intent.getData()


若要在应用间共享文件,您应发送一项 content://URI(代替file://URI),并授予 URI 临时访问权限。

FileProvider这个类就是把一个文件File,转换为 content://URI的

FileProviderContentProvider子类,所以FileProvider的使用方法,和ContentProvider使用基本上是一样的

如何共享文件,简单5步:

1、在AndroidManifest.xml中<application>标签下声明一个provider

<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="app的包名.fileProvider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>


注意:

authorities:app的包名.fileProvider

grantUriPermissions:必须是true,表示授予 URI 临时访问权限 ( readPermission, writePermission, and permission attributes)

exportedtrue: The provider is available to other applications. false: The provider is not available to other applications.

resource:自定义的xml文件(下面会介绍)

2、在res目录下新建一个xml文件夹,并且新建一个file_paths的xml文件(如下图)



3、打开file_paths.xml文件,添加指定的分享目录:

在paths节点内部支持以下几个子节点,分别为:

<root-path/> 代表设备的根目录new File(“/”);

<files-path/>代表的根目录: Context.getFilesDir()

<external-path/>代表的根目录: Environment.getExternalStorageDirectory()

<cache-path/>代表的根目录: getCacheDir()

<external-files-path>代表context.getExternalFilesDirs()

<external-cache-path>代表getExternalCacheDirs()

每个节点都支持两个属性:

name:给这个访问路径起个名字

path:需要临时授权访问的相对路径(.代表所有路径)

path即为代表目录下的子目录,比如:

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


代表的目录即为:Environment.getExternalStorageDirectory()/pics,其他同理。

既然目的是使用content://uri替代file://uri,那么,content://的uri如何定义呢?总不能使用文件路径吧,那不是骗自己么~

所以,需要一个虚拟的路径对文件路径进行映射,所以需要编写个xml文件,通过path以及xml节点确定可访问的目录,通过name属性来映射真实的文件路径。

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">

<!--代表外部存储区域的根目录下的文件 Environment.getExternalStorageDirectory()/DCIM/camerademo目录-->
<external-path name="DCIM" path="DCIM/camerademo" />

<!--代表外部存储区域的根目录下的文件 Environment.getExternalStorageDirectory()/目录-->
<external-path path="." name="external_storage_root" />

<!--代表外部存储区域的根目录下的文件 Environment.getExternalStorageDirectory()/Pictures/camerademo目录-->
<external-path name="Pictures" path="Pictures/camerademo" />

<!--代表app 私有的存储区域 Context.getFilesDir()目录下的images目录 /data/user/0/com.hm.camerademo/files/images-->
<files-path name="private_files" path="images" />

<!--代表app 私有的存储区域 Context.getCacheDir()目录下的images目录 /data/user/0/com.hm.camerademo/cache/images-->
<cache-path name="private_cache" path="images" />

<!--代表app 外部存储区域根目录下的文件 Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)目录下的Pictures目录-->
<!--/storage/emulated/0/Android/data/com.xx.xxxxxx/files/Pictures-->
<external-files-path name="external_files" path="Pictures" />

<!--代表app 外部存储区域根目录下的文件 Context.getExternalCacheDir目录下的images目录-->
<!--/storage/emulated/0/Android/data/com.xx.xxxxxx/cache/images-->
<external-cache-path name="external_cache" path="" />
</paths>


下面的例子使用SDCard,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external" path="" />
</paths>


4、FileProvider API的使用

/**
* 打开相机拍照
*
* @param activity
* @return
*/
public static void openCamera(Activity activity) {

String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
.format(new Date()) + ".png";

File pictureFile = new File(Environment.getExternalStorageDirectory(), filename );

Intent mIntent = new Intent();
mIntent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

Uri contentUri = FileProvider.getUriForFile(activity, "app的包名.fileProvider", pictureFile );
//拍照结果输出到这个uri对应的file中
mIntent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
//对这个uri进行授权
mIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
//拍照结果输出到这个uri对应的file中
mIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(pictureFile ));
}

mIntent.putExtra(MediaStore.Images.Media.ORIENTATION, 0);
activity.startActivityForResult(mIntent, REQUEST_CAMERA_IMAGE);
}


核心代码就这一行了~

Uri contentUri = FileProvider.getUriForFile(activity, "app的包名.fileProvider", pictureFile );


第二个参数就是我们配置的authorities,这个很正常了,总得映射到确定的ContentProvider吧~所以需要这个参数。

第三个参数是指定的文件File

生成的uri:

content://com.xuexuan.fileprovider/external/20171201-094017.png


可以看到格式为:content://authorities/path中name的值/文件的相对路径,即name隐藏了可存储的文件夹路径。

5、对URI进行授权

上面代码有一行注释是:对这个uri进行授权。

授权有两种方式:

Intent.addFlags,该方式主要用于针对intent.setData,setDataAndType以及setClipData相关方式传递uri的。

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


2、使用函数进行授权和移除权限

grantUriPermission(String toPackage, Uri uri,

int modeFlags)函数来进行授权

revokeUriPermission(Uri uri, int modeFlags);移除权限

方式二较为麻烦,因为需要指定目标应用包名,很多时候并不清楚,所以需要通过PackageManager进行查找到所有匹配的应用,全部进行授权。不过更为稳妥~

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);
}


如果没有授权,可能会遇到这样的错误

java.lang.SecurityException: Permission Denial:
opening provider android.support.v4.content.FileProvider
from ProcessRecord{18570a 27107:com.google.android.packageinstaller/u0a26} (pid=27107, uid=10026) that is not exported from UID 10004


以下这两个问题,可参考这篇文章

为什么在Android 7 设备上,Intent的action为ACTION_IMAGE_CAPTURE,不进行授权,不会遇到Permission Denial的问题

为什么Android 4.4设备遇到权限问题,不通过addFlags这种方式解决

错误分析

报错

java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/DCIM/camerademo/20170226_110056248725175.jpg


分析

在生成Uri 的时候,指定的文件所在的路径没有包含在path所指定的路径中

疑惑

遇到这样一个到现在没有理解的问题,在小米6,MIUI 9.0 上面,使用下面的代码,进行图片的裁剪。

1、如果不使用下面的函数授权,就会出现
java.lang.SecurityException: Permission Denial
的错误

2、使用了下面的函数授权,可以正常返回。但是返回的数据是null。但是在指定路径有裁剪后的照片输出。

有大神知道问题的原因,帮忙在评论里指导我一下,十分感谢

/***
* 裁剪图片
* @param activity Activity
* @param uri 图片的Uri
*/
public static void cropPicture(Activity activity, Uri uri) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.putExtra("crop", "true");// 才能出剪辑的小方框,不然没有剪辑功能,只能选取图片
intent.putExtra("aspectX", 1); // 放大缩小比例的X
intent.putExtra("aspectY", 1);// 放大缩小比例的X   这里的比例为:   1:1
intent.putExtra("outputX", 120);  //这个是限制输出图片大小
intent.putExtra("outputY", 120);
intent.putExtra("return-data", false);
//切图大小不足输出,无黑框
intent.putExtra("scale", true);
intent.putExtra("scaleUpIfNeeded", true);
Log.e("FaceUtil", "图片path:" + uri.toString());

File imageFile = new File(uri.getPath());

//输出图片的路径
File outputImageFile = new File(Environment.getExternalStorageDirectory(),
"picture" + System.currentTimeMillis() / 1000 + ".jpg");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

Uri contentUri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".fileProvider", imageFile);
Uri outputUri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".fileProvider", outputImageFile);

intent.setDataAndType(contentUri, "image/*");

//使用函数授权,所有的包名
List<ResolveInfo> resInfoList = activity.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
activity.grantUriPermission(packageName, outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}

//裁剪后的图片,将被保存在这个uri中
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
} else {
//裁剪后的图片,将被保存在这个uri中
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputImageFile));
intent.setDataAndType(uri, "image/*");
}

intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
activity.startActivityForResult(intent, REQUEST_CROP_IMAGE);
}


参考:

Android 7.0 行为变更 通过FileProvider在应用间共享文件吧

https://developer.android.com/guide/topics/manifest/provider-element.html

Android7.0须知–应用间共享文件(FileProvider)

解决 Android N 7.0 上 报错:android.os.FileUriExposedException

FileProvider无法获取外置SD卡问题解决方案 | Failed to find configured root that contains

FileProvider 的使用(Failed to find configured root that contains/storage/emulated/0/DCIM/ )

Android中Uri和Path之间的转换
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: