Android7.0版本后 Uri和文件路径互相转换封装类,实现系统分享功能及 FileProvider详细解析和踩坑指南,与fileprovider生成的Uri无法被识别
作者:互联网
在调用系统相机、相册时,经常需要进行Uri和File路径的互相转换,并且在项目中遇到按照百度查到的处理7.0方法分享文件到微信的7.0之后版本会文件名后缀被增加了..octet.stream无法解决,最终使用强制转换方法解决问题。
文件路径转Uri
Android 7.0以下,以文件路径创建一个File对象,然后调用Uri.fromFile(file)即可获得相应的Uri。
//创建临时图片
File photoOutputFile = SDPath.getFile("temp.jpg", SDPath.PHOTO_FILE_STR);
Uri photoOutputUri = Uri.fromFile(photoOutputFile);
但是在Android 7.0 (N) 以上,对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在应用外部公开 file:// URI,即当把targetSdkVersion指定成24及之上并且在API>=24的设备上运行时,如果一项包含文件 URI 的 intent 离开应用(如分享),则应用出现故障,并出现 FileUriExposedException 异常。
android.os.FileUriExposedException: file:///XXX exposed beyond app through ClipData.Item.getUri()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
at android.content.ClipData.prepareToLeaveProcess(ClipData.java:832)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8909)
...
查看7.0文档如下
原因在于使用file://Uri会有一些风险,比如:
- 文件是私有的,接收file://Uri的app无法访问该文件。
- 在Android6.0之后引入运行时权限,如果接收file://Uri的app没有申请READ_EXTERNAL_STORAGE权限,在读取文件时会引发崩溃。
因此,google提供了FileProvider 类,使用它可以生成content://Uri来替代file://Uri,所以要在应用间共享文件,应发送一项 content:// URI,并授予 URI 临时访问权限。
FileProvider是android support v4包提供的,是ContentProvider的子类,便于将自己app的数据提供给其他app访问。
在app开发过程中需要用到FileProvider的主要有
- 相机拍照以及图片裁剪
- 调用系统应用安装器安装apk(应用升级)
- 分享文件
使用content://
Uri的优点:
- 它可以控制共享文件的读写权限,只要调用Intent.setFlags()就可以设置对方app对共享文件的访问权限,并且该权限在对方app退出后自动失效。相比之下,使用file://Uri时只能通过修改文件系统的权限来实现访问控制,这样的话访问控制是它对所有 app都生效的,不能区分app。
- 它可以隐藏共享文件的真实路径。
file://到content://的转换规则:
a.替换前缀:把file://替换成content://${android:authorities}。
b.匹配和替换
- 遍历<paths>的子节点,找到最大能匹配上文件路径前缀的那个子节点。
- 用path的值替换掉文件路径里所匹配的内容。
c.文件路径剩余的部分保持不变.
解决方案
①定义FileProvider。在AndroidManifest.xml中加上自定义权限的ContentProvider,在<application>
节点中添加<provider>
如下
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.php.demo.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
说明:
android:authorities="com.php.demo.FileProvider" 用来标识provider的唯一标识,在同一部手机上一个"authority"串只能被一个app使用,冲突的话会导致app无法安装。我们可以利用manifest placeholders(包名)来保证authority的唯一性。
android:exported="false" 是否设置为独立进程,必须设置成false
,否则运行时会报错java.lang.SecurityException: Provider must not be exported
。
android:grantUriPermissions="true" 是否拥有共享文件的临时权限,也可以在java代码中设置。
android:resource="@xml/external_storage_root" 共享文件的文件根目录,名字可以自定义
②指定路径和转换规则。FileProvider会隐藏共享文件的真实路径,将它转换成content://
Uri路径,因此,我们还需要设定转换的规则。在项目res目录下创建一个xml文件夹,里面创建一个file_paths.xml文件,上一步定义的什么名称,这里就什么名称,如图:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external_storage_root"
path="." />
<files-path
name="files-path"
path="." />
<cache-path
name="cache-path"
path="." />
<!--/storage/emulated/0/Android/data/...-->
<external-files-path
name="external_file_path"
path="." />
<!--代表app 外部存储区域根目录下的文件 Context.getExternalCacheDir目录下的目录-->
<external-cache-path
name="external_cache_path"
path="." />
<!--配置root-path。这样子可以读取到sd卡和一些应用分身的目录,否则微信分身保存的图片,就会导致 java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/999/tencent/MicroMsg/WeiXin/export1544062754693.jpg,在小米6的手机上微信分身有这个crash,华为没有
-->
<root-path
name="root-path"
path="" />
/paths>
这个配置的标签参照FileProvider里面的TAG配置。
root-path 对应DEVICE_ROOT,也就是File DEVICE_ROOT = new File("/"),即根目录,一般不需要配置。
files-path对应 content.getFileDir() 获取到的目录。
cache-path对应 content.getCacheDir() 获取到的目录
external-path对应 Environment.getExternalStorageDirectory() 指向的目录。
external-files-path对应 ContextCompat.getExternalFilesDirs() 获取到的目录。
external-cache-path对应 ContextCompat.getExternalCacheDirs() 获取到的目录。
TAG | Value | Path |
TAG_ROOT_PATH | root-path | / |
TAG_FILES_PATH | files-path | /data/data/<包名>/files |
TAG_CACHE_PATH | cache-path | /data/data/<包名>/cache |
TAG_EXTERNAL | external-path | /storage/emulate/0 |
TAG_EXTERNAL_FILES | external-files-path | /storage/emulate/0/Android/data/<包名>/files |
TAG_EXTERNAL_CACHE | external-cache-path | /storage/emulate/0/Android/data/<包名>/cache |
首先介绍些基础知识:Android的文件系统和MediaStore类的使用
外部存储的公共目录
DIRECTORY_MUSIC:音乐类型 /storage/emulate/0/music
DIRECTORY_PICTURES:图片类型
DIRECTORY_MOVIES:电影类型
DIRECTORY_DCIM:照片类型,相机拍摄的照片视频都在这个目录(digital camera in memory) /storage/emulate/0/DCIM
DIRECTORY_DOWNLOADS:下载文件类型 /storage/emulate/0/downloads
DIRECTORY_DOCUMENTS:文档类型
DIRECTORY_RINGTONES:铃声类型
DIRECTORY_ALARMS:闹钟提示音类型
DIRECTORY_NOTIFICATIONS:通知提示音类型
DIRECTORY_PODCASTS:播客音频类型
这些可以通过Environment的getExternalStoragePublicDirectory()来获取
安卓系统会在每次开机之后扫描所有文件并分类整理存入数据库,记录在MediaStore这个类里,通过这个类就可以快速的获得相应类型的文件。当然这个类只是给你一个uri,提取文件的操作还是要通过Curosr这个类来完成。获得Cursor对象实例的方法必须通过Context实例获得ContextResolver对象,通过这个对象调用query方法。
就是这样 mycontext.getContentResolver().query(uri, columns, selection, null, null);
mycontext通过活动实例获取,其他的就没必要说了 说说参数(官方文档里有详细说明),第一个就是uri说白了就是地址,第二个是选择哪些列(列的名字在官方文档里有需要哪个写那个就够了),第三个是选择指定的行一般都是通过mimetype去选择(传入的参数是sql语句的字符串),第四个没用过,第五个就是排序的要求和第三个差不多 注意前三个参数有点问题就会空指针。
下面贴一下通过MediaStore类获得URI的代码
private Uri getContentUri(FileCategory cat) {
Uri uri;
String volumeName = "external";
switch(cat) {
case Theme:
case Doc:
case Zip:
case Apk:
uri = Files.getContentUri(volumeName);
break;
case Music:
uri = Audio.Media.getContentUri(volumeName);
break;
case Video:
uri = Video.Media.getContentUri(volumeName);
break;
case Picture:
uri = Images.Media.getContentUri(volumeName);
break;
default:
uri = null;
}
Log.e(LOG_CURSOR, "getContentUri");
return uri;
}
接下来以系统分享功能为例,解决“获取资源失败”和fileprovider生成的uri地址,应用不能识别问题,是要把uri地址转换一下。
要调用 Android 系统内建的分享功能,主要有三步流程:
- 创建一个 Intent ,指定其 Action 为 Intent.ACTION_SEND,表示要创建一个发送指定内容的隐式意图。
- 然后指定需要发送的内容和类型,设置分享的文本内容或文件的Uri,以及文件的类型,便于是支持该类型内容的应用打开。
- 最后向系统发送隐式意图,开启系统分享选择器,分享完成后收到结果返回。
知道大致的实现流程后,其实只要解决下面几个问题后就可以具体实施了。
确定要分享的内容类型
这其实是直接决定了最终的实现形态,我们知道常见的使用场景中,只是为了在应用间分享图片和一些文件,那对于那些只是分享文本的产品而言,两者实现起来要考虑的问题完全不同。
所以为了解决这个问题,我们可以预先定好支持的分享内容类型,针对不同类型可以进行不同的处理。
@StringDef({ShareContentType.TEXT, ShareContentType.IMAGE,
ShareContentType.AUDIO, ShareContentType.VIDEO, ShareContentType.File})
@Retention(RetentionPolicy.SOURCE)
@interface ShareContentType {
/**
* Share Text
*/
final String TEXT = "text/plain";
/**
* Share Image
*/
final String IMAGE = "image/*";
/**
* Share Audio
*/
final String AUDIO = "audio/*";
/**
* Share Video
*/
final String VIDEO = "video/*";
/**
* Share File
*/
final String File = "*/*";
}`
上述一共定义了5种类别的分享内容,基本能覆盖常见的使用场景。在调用分享接口时可以直接指定内容类型,比如像文本、图片、音视频、及其他各种类型文件。
确定分享的内容来源
比如调用系统相机进行拍照或录制音视频,要传入一个生成目标文件的 Uri
private static final int REQUEST_FILE_SELECT_CODE = 100;
/**
* 打开系统相机进行拍照
*/
private void openSystemCamera() {
//调用系统相机
Intent takePhotoIntent = new Intent();
takePhotoIntent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePhotoIntent.resolveActivity(getPackageManager()) == null) {
Toast.makeText(this, "当前系统没有可用的相机应用", Toast.LENGTH_SHORT).show();
return;
}
String fileName = "TEMP_" + System.currentTimeMillis() + ".jpg";
File photoFile = new File(FileUtil.getPhotoCacheFolder(), fileName);
// 7.0和以上版本的系统要通过 FileProvider 创建一个 content 类型的 Uri
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
currentTakePhotoUri = FileProvider.getUriForFile(this, getPackageName() + ".fileProvider", photoFile);
takePhotoIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION|);
} else {
currentTakePhotoUri = Uri.fromFile(photoFile);
}
//将拍照结果保存至 outputFile 的Uri中,不保留在相册中
takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, currentTakePhotoUri);
startActivityForResult(takePhotoIntent, TAKE_PHOTO_REQUEST_CODE);
}
// 调用系统相机进行拍照与上面通过文件选择器获得文件 uri 的方式类似
// 在 onActivityResult 进行回调处理,此时 Uri 是你 FileProvider 中指定的,注意与文件选择器获取的 Uri 的区别。
分享文件 Uri 的处理
要对应用进行临时访问 Uri 的授权才行,不然会提示权限缺失。对于要分享系统返回的 Uri 我们可以这样进行处理:
// 可以对发起分享的 Intent 添加临时访问授权
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 也可以这样:由于不知道最终用户会选择哪个app,所以授予所有应用临时访问权限
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
List<ResolveInfo> resInfoList = activity.getPackageManager().queryIntentActivities(shareIntent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
activity.grantUriPermission(packageName, shareFileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}
需要注意的是对于自定义 FileProvider 返回 Uri 的处理,即使是设置临时访问权限,但是分享到第三方应用也会无法识别该 Uri
典型的场景就是,我们如果把自定义 FileProvider 的返回的 Uri 设置分享到微信或 QQ 之类的第三方应用,会提示文件不存在,这是因为他们无法识别该 Uri。
关于这个问题的处理其实跟下面要说的把文件路径变成系统返回的 Uri 一样,我们只需要把自定义 FileProvider 返回的 Uri 变成第三方应用可以识别系统返回的 Uri 就行了。
创建 FileProvider 时需要传入一个 File 对象,所以直接可以知道文件路径,那就把问题都转换成了:如何通过文件路径获取系统返回的 Uri
本人在项目中获取本地文件如下:
Intent share = new Intent(Intent.ACTION_SEND);
File file = new File(filePath);
Uri contentUri = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
share.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
contentUri = DealFileClass.getFileUri(getActivity(),DealFileClass.ShareContentType.File,file);
share.putExtra(Intent.EXTRA_STREAM, contentUri);
share.setType("application/pdf");// 此处可发送多种文件
} else {
share.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
share.setType("application/pdf");// 此处可发送多种文件
}
try{
startActivity(Intent.createChooser(share, "Share"));
} catch (Exception e) {
e.printStackTrace();
}
下面是根据传入的 File 对象和类型来查询系统 ContentProvider 来获取相应的 Uri,已经按照不同文件类型在不同系统版本下的进行了适配。
其中 forceGetFileUri
方法是通过反射实现的,处理 7.0 以上系统的特殊情况下的兼容性,一般情况下不会调用到。Android 7.0 开始不允许 file://
Uri 的方式在不同的 App 间共享文件,但是如果换成 FileProvider 的方式依然是无效的,我们可以通过反射把该检测干掉。
public static Uri getFileUri (Context context, @ShareContentType String shareContentType, File file){
if (context == null) {
Log.e(TAG,"getFileUri current activity is null.");
return null;
}
if (file == null || !file.exists()) {
Log.e(TAG,"getFileUri file is null or not exists.");
return null;
}
Uri uri = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
uri = Uri.fromFile(file);
} else {
if (TextUtils.isEmpty(shareContentType)) {
shareContentType = "*/*";
}
switch (shareContentType) {
case ShareContentType.IMAGE :
uri = getImageContentUri(context, file);
break;
case ShareContentType.VIDEO :
uri = getVideoContentUri(context, file);
break;
case ShareContentType.AUDIO :
uri = getAudioContentUri(context, file);
break;
case ShareContentType.File :
uri = getFileContentUri(context, file);
break;
default: break;
}
}
if (uri == null) {
uri = forceGetFileUri(file);
}
return uri;
}
private static Uri getFileContentUri(Context context, File file) {
String volumeName = "external";
String filePath = file.getAbsolutePath();
String[] projection = new String[]{MediaStore.Files.FileColumns._ID};
Uri uri = null;
Cursor cursor = context.getContentResolver().query(MediaStore.Files.getContentUri(volumeName), projection,
MediaStore.Images.Media.DATA + "=? ", new String[] { filePath }, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID));
uri = MediaStore.Files.getContentUri(volumeName, id);
}
cursor.close();
}
return uri;
}
private static Uri getImageContentUri(Context context, File imageFile) {
String filePath = imageFile.getAbsolutePath();
Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[] { MediaStore.Images.Media._ID }, MediaStore.Images.Media.DATA + "=? ",
new String[] { filePath }, null);
Uri uri = null;
if (cursor != null) {
if (cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/images/media");
uri = Uri.withAppendedPath(baseUri, "" + id);
}
cursor.close();
}
if (uri == null) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA, filePath);
uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
}
return uri;
}
private static Uri getVideoContentUri(Context context, File videoFile) {
Uri uri = null;
String filePath = videoFile.getAbsolutePath();
Cursor cursor = context.getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
new String[] { MediaStore.Video.Media._ID }, MediaStore.Video.Media.DATA + "=? ",
new String[] { filePath }, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/video/media");
uri = Uri.withAppendedPath(baseUri, "" + id);
}
cursor.close();
}
if (uri == null) {
ContentValues values = new ContentValues();
values.put(MediaStore.Video.Media.DATA, filePath);
uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
}
return uri;
}
private static Uri getAudioContentUri(Context context, File audioFile) {
Uri uri = null;
String filePath = audioFile.getAbsolutePath();
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
new String[] { MediaStore.Audio.Media._ID }, MediaStore.Audio.Media.DATA + "=? ",
new String[] { filePath }, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/audio/media");
uri = Uri.withAppendedPath(baseUri, "" + id);
}
cursor.close();
}
if (uri == null) {
ContentValues values = new ContentValues();
values.put(MediaStore.Audio.Media.DATA, filePath);
uri = context.getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
}
return uri;
}
private static Uri forceGetFileUri(File shareFile) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
@SuppressLint("PrivateApi")
Method rMethod = StrictMode.class.getDeclaredMethod("disableDeathOnFileUriExposure");
rMethod.invoke(null);
} catch (Exception e) {
Log.e(TAG, Log.getStackTraceString(e));
}
}
return Uri.parse("file://" + shareFile.getAbsolutePath());
}
此外,如若uri要转换为文件路径则可如下处理:
Intent intent = getIntent();
String action = intent.getAction();
if (Intent.ACTION_VIEW.equals(action)) {
Uri uri = intent.getData();
String filename = uri.getPath();
if (String.valueOf(uri) != null && String.valueOf(uri).contains("content")) {
boolean kkk = false;
try{
filename = CommonUtils.getFilePathFromContentUri(uri,this.getContentResolver());
if(CommonUtils.isEmpty(filename)){
kkk = true;
}
}catch (Exception e){
e.printStackTrace();
kkk = true;
}
if(kkk){
filename = ProviderUtils.getFPUriToPath(this,uri);
}
}
}
其中,getFilePathFromContentUri如下:
/**
* 将uri转换成真实路径
*
* @param selectedVideoUri
* @param contentResolver
* @return
*/
public static String getFilePathFromContentUri(Uri selectedVideoUri,
ContentResolver contentResolver) {
String filePath = "";
String[] filePathColumn = {MediaColumns.DATA};
Cursor cursor = contentResolver.query(selectedVideoUri, filePathColumn,
null, null, null);
// 也可用下面的方法拿到cursor
// Cursor cursor = this.context.managedQuery(selectedVideoUri,
// filePathColumn, null, null, null);
// cursor.moveToFirst();
//
// int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
// filePath = cursor.getString(columnIndex);
if (cursor != null) {
if (cursor.moveToFirst()) {
int id = cursor.getColumnIndex(filePathColumn[0]);
if(id > -1)
filePath = cursor.getString(id);
}
cursor.close();
}
return filePath;
}
ProviderUtils类文件内容如下:
public class ProviderUtils {
public static String getFPUriToPath(Context context, Uri uri) {
try {
List<PackageInfo> packs = context.getPackageManager().getInstalledPackages(PackageManager.GET_PROVIDERS);
if (packs != null) {
String fileProviderClassName = FileProvider.class.getName();
for (PackageInfo pack : packs) {
ProviderInfo[] providers = pack.providers;
if (providers != null) {
for (ProviderInfo provider : providers) {
if (uri.getAuthority().equals(provider.authority)) {
if (provider.name.equalsIgnoreCase(fileProviderClassName)) {
Class<FileProvider> fileProviderClass = FileProvider.class;
try {
Method getPathStrategy = fileProviderClass.getDeclaredMethod("getPathStrategy", Context.class, String.class);
getPathStrategy.setAccessible(true);
Object invoke = getPathStrategy.invoke(null, context, uri.getAuthority());
if (invoke != null) {
String PathStrategyStringClass = FileProvider.class.getName() + "$PathStrategy";
Class<?> PathStrategy = Class.forName(PathStrategyStringClass);
Method getFileForUri = PathStrategy.getDeclaredMethod("getFileForUri", Uri.class);
getFileForUri.setAccessible(true);
Object invoke1 = getFileForUri.invoke(invoke, uri);
if (invoke1 instanceof File) {
String filePath = ((File) invoke1).getAbsolutePath();
return filePath;
}
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
break;
}
break;
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
最后欢迎大家关注我个人公众号,可以一起交流成长。
标签:cursor,String,uri,MediaStore,Uri,Android7.0,null,fileprovider 来源: https://blog.csdn.net/P876643136/article/details/88077803