一、背景
传统上,应用需读取文件信息时,通常通过申请存储权限或所有文件访问权限,由用户主动授权后,应用才可以读取手机存储的文件。该方案存在诸多痛点,例如:
- 应用一般不需要读取手机上的所有文件数据,但申请并获得对应的权限,将获得读取存储空间上所有文件数据的能力,存在申请数据范围过大的问题;
- 与此同时,用户出于隐私考虑,可能会拒绝授权,导致应用相关功能不可用,影响应用的用户体验;
- 此外,用户如果多次拒绝授权,应用则需引导用户,跳转到设置中,手动授权,才能继续正常使用应用相关功能,影响应用用户体验。
为解决上述问题,文件Picker应运而生。应用可通过接入文件Picker,弹出系统级的文件选择界面,由用户手动选择需要的文件,授权给三方应用读取,具有如下显著优势:
- 应用不再需要申请完整的存储权限,解决用户拒绝授权导致应用功能不可用问题;
- 应用只能读取用户选择的文件数据,满足用户最小化授权的要求。
二、功能介绍
应用通过接入文件Picker,在用户使用相关功能时,弹出系统级的文件Picker选择界面,由用户手动选择需要授权的文件授权给三方应用,三方应用读取到用户选择文件数据,继续使用应用功能,文件Picker支持应用进行单选和多选,最多支持选择100个文件。

三、适用范围
当前支持最新视觉效果文件Picker的操作系统版本如下:
- ColorOS 16(基于Android 16)及以上
- HyperOS 3(基于Android 16)及以上
- OriginOS 6(基于Android 16)及以上
- MagicOS 10(基于Android 16)及以上
四、使用说明
以下详细说明文件Picker的接口。
1. 文件Intent参数构造
应用可按下述参数说明,构造Intent,使用startActivityForResult(Intent intent, int requestCode),拉起文件Picker界面。
1.1 action
构造Intent,应用需传入action参数,必选。
| 构建参数 | action |
| 参数类型 | String |
| 构造API | new Intent(String actionName) |
| 取值 | Intent.ACTION_OPEN_DOCUMENT = "android.intent.action.OPEN_DOCUMENT" |
调用示例:
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);1.2 type
使用type参数,用于指定文件的数据类型,必须指定。
| 构建参数 | type |
| 参数类型 | String |
| 构造API | Intent.setType(String type) |
| 取值范围 | 文件类型字符串,如"text/plain","application/pdf","*/*"等 |
调用示例:
intent.setType("text/plain");1.3 Extras:EXTRA_MIME_TYPES
应用可选传入MIME 类型数组,限制用户只能选择这些类型的文件,配合type使用
| 构建参数 | EXTRA_MIME_TYPES |
| 参数类型 | String |
| 构造API | Intent.putExtra(String name, String[] filterFileType) |
| 取值范围 | name参数:Intent.EXTRA_MIME_TYPES name参数说明:限制用户选择的文件类型 filterFileType参数取值范围:字符串数组 filterFileType参数说明:文件类型,如new String[] {"image/*", // 所有图片 "application/pdf" // PDF文件 } |
调用示例:
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{
"text/*",
"application/pdf"});1.4. Extras:EXTRA_ALLOW_MULTIPLE
应用可选传入是否多选的EXTRA_ALLOW_MULTIPLE参数,如不传入,则默认为单选。
| 构造参数 | EXTRA_ALLOW_MULTIPLE |
| 参数类型 | boolean |
| 构造API | Intent.putExtra(String name, boolean value) |
| 取值范围 | name参数:Intent.EXTRA_ALLOW_MULTIPLE name参数说明:是否多选 value参数取值范围:true/false value参数说明:true对应多选,false对应单选 |
备注:文件Picker多选最大支持可选100个文件。
调用示例:
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)2. Intent返回值
2.1. requestCode
在 onActivityResult 回调中,requestCode 值就是传入 startActivityForResult 的 requestCode,关联回调的结果与对应的请求。
| 返回值 | requestCode |
| 参数类型 | int |
| 取值 | 与startActivityForResult(Intent intent, int requestCode)时传入的requestCode一致,代表对应请求的回调。 |
2.2. resultCode
| 返回值 | resultCode |
| 参数类型 | int |
| 取值范围 | Activity.RESULT_OK,成功 Activity.RESULT_CANCELED,被取消 |
2.3. data
| 返回值 | data |
| 参数类型 | intent |
| 数据处理 | data.getData()拿到Uri,之后可通过ContentResolver.openInputStream读取文件数据 |
3. 持久化授权
持久化授权takePersistableUriPermission 是 Android ContentResolver 类中的一个方法,用于获取针对给定 URI 的长期持久权限授予,例如通过 ACTION_OPEN_DOCUMENT以选择特定文件,使应用能够在设备重启和应用重新启动后保留对用户所选文档的访问权限。
// 在onActivityResult中
final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(uri, takeFlags); 4. androidX库接口
应用接入时,可以使用androidX库的封装接口:
五、使用示例
1. 兼容性适配
1.1 移除AndroidManifest权限配置
在支持文件Picker的系统版本,应用不再需要申请存储相关权限时:
- 应用可移除
AndroidManiest.xml中的读取存储相关权限申请
<!--<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />-->
<!--<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />-->- 应用也可在AndroidManifest.xml中配置读取存储相关权限最高可用的Android版本,以最高支持在Android 15(SDK版本号35)申请存储相关权限。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="35" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:maxSdkVersion="35" />1.2 判断系统是否支持
判断系统级文件Picker是否可用参考代码
public boolean isFilePickerAvailable(Intent intent) {
return getPackageManager().resolveActivity(intent, 0) != null;
}基于Android 16及以上的国内机型,为厂商优化后的新UI风格,体验更好。
应用可在不支持文件Picker的操作系统,继续申请读取文件相关权限,参考兼容代码如下:
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.setType("text/plain")
if (isFilePickerAvaiable(intent)) {
// 支持,拉起文件Picker
startActivityForResult(intent, REQUEST_CODE);
} else {
// 不支持,申请权限等方式
}2. 打开文件
Java用法示例
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
private static final String LOG_TAG = "PickerDemo";
public void pickOpenFile() {
// 构造intent,设置action
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"text/plain"});
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) // 是否支持多选,true支持,false不支持
openActivityResultLauncher.launch(intent);
}
ActivityResultLauncher<Intent> openActivityResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
if (result.getResultCode() == Activity.RESULT_OK) {
Intent data = result.getData();
Uri uri = data.getData();
if (uri != null) { // 单选
try (InputStream inputStream = getContext().getContentResolver().openInputStream(uri)) {
if (inputStream != null) {
byte[] bytes = inputStream.readAllBytes();
Log.d(LOG_TAG, "Selected URI: " + uri + ", read success:" + new String(bytes));
}
} catch (IOException e) {
Log.d(LOG_TAG, "Selected URI: " + uri+", read failed");
}
} else { // 多选
ClipData clipData = data.getClipData();
if (clipData != null) {
for (int i= 0; i < clipData.getItemCount(); i++) {
Log.d(LOG_TAG, "Selected URI is " + clipData.getItemAt(i).getUri());
}
}
}
}
}
});Kotlin用法示例
import androidx.activity.result.contract.ActivityResultContracts
private val LOG_TAG = "PickerDemo"
fun pickOpenFile() {
// 构造intent,设置action
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.setType("text/plain")
intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/plain"))
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) // 是否支持多选,true支持,false不支持
openActivityResultLauncher.launch(intent)
}
private val openActivityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
if (data != null) {
val uri = data.data
try {
if (uri != null) { // 单选
getContentResolver().openInputStream(uri).use { inputStream ->
if (inputStream != null) {
val bytes: ByteArray = inputStream.readAllBytes()
Log.d(LOG_TAG, "Selected URI: " + uri + ", read success:" + String(bytes))
}
}
} else { // 多选
val clipData = data.clipData
if (clipData != null) {
for (i in 0..<clipData!!.itemCount) {
Log.d(LOG_TAG,"Selected URI is " + clipData.getItemAt(i).uri)
}
}
}
} catch (e: IOException) {
Log.e(LOG_TAG, "Selected URI: $uri, read failed")
}
}
}
}六、参考
- 使用存储访问框架打开文件:https://developer.android.com/guide/topics/providers/document-provider?hl=zh-cn
- 常用筛选的文件类型: https://mime.wcode.net/zh-hans/
| 文件类型 | 含义 |
| image/* | 所有图片 |
| audio/* | 所有音频 |
| video/* | 所有视频 |
| text/* | 所有文本 |
| text/plain | 纯文本 |
| application/pdf | PDF文档 |
| application/zip | ZIP压缩包 |
| text/html | HTML 文档 |
| application/xhtml+xml | XHTML 文档 |
| image/gif | GIF 图像 |
| image/jpeg | JPEG 图像 |
| image/bmp | BMP 图像 |
| image/png | PNG 图像 |
| video/mpeg | MPEG 动画 |
| application/octet-stream | 任意二进制数据 |
| application/msword | Microsoft Word 文件 |
| video/3gpp | 3GP 视频 |
| application/vnd.android.package-archive | APK 文件 |
| audio/mpeg | MP3 音频 |
| video/mp4 | MP4 视频 |
| application/vnd.ms-powerpoint | PPT 文件 |
| application/vnd.ms-excel | Excel 文件 |
| application/json | JSON 数据 |
| application/xml | XML 数据 |
| \*/\* | 所有文件类型 |
该适配指南与金标联盟官网适配指南一致,您也可查阅金标联盟官网中公示适配指南。
金标联盟官方文档地址:https://www.itgsa.com/doc/6631953378231296