文件管理器的组成
调用系统自带的文件管理器有如下几个选项,分为两类
- 系统自带的文件管理器
- 第三方集成到系统管理器中
下面是调用选择文件方法后,调用系统文件管理器出来的界面
网上可以搜到很多 Uri 转路径的方法,但都是互相抄袭根本没经过验证的。现在最新是 Android 10,而网上那些方法,大多连 Android 7 引入文件权限(fileProvider)都不支持。本着认真仔细的研究,我安装了如下文件浏览器。
不同类别 Uri 的格式
系统自带的文件管理器
列名 | uri |
---|---|
1. 图片 | content://com.android.providers.media.documents/document/image%3A1598915 |
2. 视频 | content://com.android.providers.media.documents/document/video%3A1594850 |
3. 音频 | content://com.android.providers.media.documents/document/audio%3A920365 |
4. 最近 | content://com.android.providers.media.documents/document/image%3A1598915 |
5. 下载 | content://com.android.providers.downloads.documents/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2Ftest.txt |
6. 手机(vivo X20A) | content://com.android.externalstorage.documents/document/primary%3Atest.txt |
第三方集成到系统管理器中
列名 | uri |
---|---|
7. 视频 | content://media/external/video/media/1594849 |
8. 选择曲目 | content://media/external/audio/media/1151693 |
9. 录音机 | 无 |
10. 相册 | content://media/external/images/media/1508729 |
11. WPS Office | content://cn.wps.moffice_eng.fileprovider/external/test.txt |
12. 文件管理器 | file:///storage/emulated/0/test.txt |
13. ES文件管理器 | content://com.estrongs.files/storage/emulated/0/test.txt |
14. ES文件管理器 | content://com.gzhesnet.filemanager.FILE_PROVIDER/storage_root/test.txt |
15. 文件管理器 | content://com.jinghong.fileguanlijh.FILE_PROVIDER/storage_root/Android/log.txt |
16. 文件管理 | content://com.tencent.mtt.fileprovider/QQBrowser/test.txt |
17. 7Zipper | content://org.joa.zipperplus7//storage/emulated/0/test.txt |
有通过上面的 Uri 文本可以发现有如下几个共同点
- 自带的 图片、音频、视频、最近 的提供者都是 com.android.providers.media.documents,%3A是冒号,后面跟的是 ID
- 下载的提供者是 com.android.providers.downloads.documents
- 手机文件夹的提供者是 com.android.externalstorage.documents
- 视频、曲目、相机 都来自 media
- 其它第三方文件管理器的格式基本一致:content:// 第三方 fileprovider / 暴露路径名 / 文件相对SD卡的具体路径。
- 只有一个估计是旧版的文件管理器使用的是 file 协议下的绝对路径。
代码讲解
uri 拆分
以 content://com.android.providers.media.documents/document/image%3A1598915 为例
函数 | 返回值 | 说明 |
---|---|---|
uri.getScheme() | content | uri 协议 |
uri.getAuthority() | com.android.providers.media.documents | 文件提供器标识 |
uri.getPath() | /document/image:159815 | 获取文件提供器之后的路径 |
uri.getPathSegments() | [document, image:1598915] | 获取文件提供器之后的路径,以File.separator切分成数组(自动解码) |
Uri.withAppendedPath(uri, segment) | content://com.android.providers.media.documents/document/image%3A1598915/segment | 在uri最后添加一个子路径 |
uri 分类
- 系统的内容提供器
- 第三方的内容提供器
- 旧式file类型的uri
系统的内容提供器
系统内容提供器创建的文件都是来自 DocumentsProvider,使用 DocumentsContract.isDocumentUri(context, uri) 可以判断该 uri 是否来自 系统内容提供器,源码如下
首先 uri 的协议必须是 content。
public static final String SCHEME_CONTENT = "content";
public static boolean isContentUri(@Nullable Uri uri) {
return uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme());
}
其次,必须来自文档提供器。通过文件提供器的意图去系统里查询,对于查询到的结果,一一比对。如果存在一致的提供器标识(authority),则返回 true
/**
* Intent action used to identify {@link DocumentsProvider} instances. This
* is used in the {@code <intent-filter>} of a {@code <provider>}.
*/
public static final String PROVIDER_INTERFACE = "android.content.action.DOCUMENTS_PROVIDER";
private static boolean isDocumentsProvider(Context context, String authority) {
final Intent intent = new Intent(PROVIDER_INTERFACE);
final List<ResolveInfo> infos = context.getPackageManager()
.queryIntentContentProviders(intent, 0);
for (ResolveInfo info : infos) {
if (authority.equals(info.providerInfo.authority)) {
return true;
}
}
return false;
}
DocumentsContract.isDocumentUri(Context context, Uri uri) 函数,先判断 uri 的类型是content,并且内容提供器是 DocumentsProvider。然后通过 uri.getPathSegments() 获取被 File.separator 分隔路径片段并自动解码(注意到:%3A是:;%2F是/ )所以我们上面给出的 uri 的 paths.size 一般都等于 2,然后再判断第一个片段的值是否是 document
@UnsupportedAppUsage
private static final String PATH_DOCUMENT = "document";
@UnsupportedAppUsage
private static final String PATH_TREE = "tree";
public static boolean isDocumentUri(Context context, @Nullable Uri uri) {
if (isContentUri(uri) && isDocumentsProvider(context, uri.getAuthority())) {
final List<String> paths = uri.getPathSegments();
if (paths.size() == 2) {
return PATH_DOCUMENT.equals(paths.get(0));
} else if (paths.size() == 4) {
return PATH_TREE.equals(paths.get(0)) && PATH_DOCUMENT.equals(paths.get(2));
}
}
return false;
}
DocumentsContract.getDocumentId(Uri uri) 这个函数就是获取 DocumentsProvider 提供器的 uri 的 path 部分,然后如果 第一个路径片段是 document,则取第二个部分
public static String getDocumentId(Uri documentUri) {
final List<String> paths = documentUri.getPathSegments();
if (paths.size() >= 2 && PATH_DOCUMENT.equals(paths.get(0))) {
return paths.get(1);
}
if (paths.size() >= 4 && PATH_TREE.equals(paths.get(0))
&& PATH_DOCUMENT.equals(paths.get(2))) {
return paths.get(3);
}
throw new IllegalArgumentException("Invalid URI: " + documentUri);
}
%3A,也就是冒号之前的,共有5种格式,分别是
- image
- video
- audio
- raw
- primary
//如果是document类型的Uri,通过document id处理,内部会调用Uri.decode(docId)进行解码
String docId = DocumentsContract.getDocumentId(uri);
//primary:Azbtrace.txt
//video:A1283522
String[] splits = docId.split(":");
Log.i(TAG, "docId " + docId + ", " + Arrays.toString(splits));
String type = null, id = null;
if(splits.length == 2) {
type = splits[0];
id = splits[1];
}
系统的 DocumentsProvider 一共有3种
- com.android.providers.media.documents(image、video、audio、document)
- com.android.providers.downloads.documents(raw)
- com.android.externalstorage.documents(primary)
switch (uri.getAuthority()) {
case "com.android.externalstorage.documents":
if("primary".equals(type)) {
path = Environment.getExternalStorageDirectory() + File.separator + id;
}
break;
case "com.android.providers.downloads.documents":
if("raw".equals(type)) {
path = id;
} else {
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
path = getMediaPathFromUri(context, contentUri, null, null);
}
break;
case "com.android.providers.media.documents":
Uri externalUri = null;
switch (type) {
case "image":
externalUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
break;
case "video":
externalUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
break;
case "audio":
externalUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
break;
case "document":
externalUri = MediaStore.Files.getContentUri("external");
break;
}
if(externalUri != null) {
String selection = "_id=?";
String[] selectionArgs = new String[]{ id };
path = getMediaPathFromUri(context, externalUri, selection, selectionArgs);
}
break;
}
第三方的内容提供器
非 DocumentsProvider,并且协议类型是 content
if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) {
……
}
以下这些字段的值都是 _data
- MediaStore.Images.Media.DATA
- MediaStore.Audio.Media.DATA
- MediaStore.Video.Media.DATA
- MediaStore.MediaColumns.DATA
ContentResolver resolver = context.getContentResolver();
String[] projection = new String[]{ MediaStore.MediaColumns.DATA };
Cursor cursor = resolver.query(uri, projection, selection, selectionArgs, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
try {
int index = cursor.getColumnIndexOrThrow(projection[0]);
if (index != -1) path = cursor.getString(index);
Log.i(TAG, "getMediaPathFromUri query " + path);
} catch (IllegalArgumentException e) {
e.printStackTrace();
path = null;
} finally {
cursor.close();
}
}
}
当通过相机或第三方文件管理器获取文件时就会报:IllegalArgumentException: column’_data’ does not exist. Available columns: [],而ES文件浏览器,通过resolver去query,会报RuntimeException。因此,它们要提出来在 query 之前做特殊处理。
对于第三方提供的路径,有2种情况
- authority 后面直接的是实际路径
- authority 后面跟了一个应用路径,然后是相对sd卡的路径
String path;
String authroity = uri.getAuthority();
path = uri.getPath();
String sdPath = Environment.getExternalStorageDirectory().getAbsolutePath();
if(!path.startsWith(sdPath)) {
int sepIndex = path.indexOf(File.separator, 1);
if(sepIndex == -1) path = null;
else {
path = sdPath + path.substring(sepIndex);
}
}
旧式file类型的uri
协议类型是 file
if(ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme()) {
……
}
方法 | 说明 |
---|---|
Uri.parse(String uriString) | uri 字符串转uri |
Uri.fromFile(File file) | 路径转 uri |
uri.getPath() | uri 转路径 |
假设路径为 storage/emulated/0/test.txt,通过 Uri.fromFile 就可以转成 file:///storage/emulated/0/test.txt
而 file:///storage/emulated/0/test.txt 这种格式的 Uri,直接调用 uri.getPath,就会得到 storage/emulated/0/test.txt
完整代码如下
public static String getPathFromUri(Context context, Uri uri) {
String path = null;
if (DocumentsContract.isDocumentUri(context, uri)) {
//如果是document类型的Uri,通过document id处理,内部会调用Uri.decode(docId)进行解码
String docId = DocumentsContract.getDocumentId(uri);
//primary:Azbtrace.txt
//video:A1283522
String[] splits = docId.split(":");
String type = null, id = null;
if(splits.length == 2) {
type = splits[0];
id = splits[1];
}
switch (uri.getAuthority()) {
case "com.android.externalstorage.documents":
if("primary".equals(type)) {
path = Environment.getExternalStorageDirectory() + File.separator + id;
}
break;
case "com.android.providers.downloads.documents":
if("raw".equals(type)) {
path = id;
} else {
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
path = getMediaPathFromUri(context, contentUri, null, null);
}
break;
case "com.android.providers.media.documents":
Uri externalUri = null;
switch (type) {
case "image":
externalUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
break;
case "video":
externalUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
break;
case "audio":
externalUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
break;
case "document":
externalUri = MediaStore.Files.getContentUri("external");
break;
}
if(externalUri != null) {
String selection = "_id=?";
String[] selectionArgs = new String[]{ id };
path = getMediaPathFromUri(context, externalUri, selection, selectionArgs);
}
break;
}
} else if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) {
path = getMediaPathFromUri(context, uri, null, null);
} else if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme())) {
//如果是file类型的Uri(uri.fromFile),直接获取图片路径即可
path = uri.getPath();
}
//确保如果返回路径,则路径合法
return path == null ? null : (new File(path).exists() ? path : null);
}
private static String getMediaPathFromUri(Context context, Uri uri, String selection, String[] selectionArgs) {
String path;
String authroity = uri.getAuthority();
path = uri.getPath();
String sdPath = Environment.getExternalStorageDirectory().getAbsolutePath();
if(!path.startsWith(sdPath)) {
int sepIndex = path.indexOf(File.separator, 1);
if(sepIndex == -1) path = null;
else {
path = sdPah + path.substring(sepIndex);
}
}
if(path == null || !new File(path).exists()) {
ContentResolver resolver = context.getContentResolver();
String[] projection = new String[]{ MediaStore.MediaColumns.DATA };
Cursor cursor = resolver.query(uri, projection, selection, selectionArgs, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
try {
int index = cursor.getColumnIndexOrThrow(projection[0]);
if (index != -1) path = cursor.getString(index);
Log.i(TAG, "getMediaPathFromUri query " + path);
} catch (IllegalArgumentException e) {
e.printStackTrace();
path = null;
} finally {
cursor.close();
}
}
}
}
return path;
}