这个 FileProvider
配置必须针对 Android 7.0+(API 24+)做兼容 ,否则在 Android 7.0 及以上版本中,通过 file://
路径访问文件会触发 FileUriExposedException
异常。结合你当前的项目(已适配 Android 10+),需要进一步完善 FileProvider
的配置,确保文件访问在全版本兼容。
一、为什么需要兼容?
Android 7.0 开始引入 StrictMode 安全策略 ,禁止应用通过 file://
格式的 Uri 向其他应用暴露文件(如调用系统相机拍照、分享文件等场景),必须使用 content://
格式的 Uri 替代,而 FileProvider
就是系统提供的用于生成 content://
Uri 的工具类。
你的现有配置已经有了 FileProvider
基础,但还需要补充两点:
- 完善
provider_paths.xml
配置(指定可共享的文件路径,适配 Android 10+ 分区存储); - 在代码中通过
FileProvider
生成 Uri(替代直接使用new File(...)
生成file://
Uri)。
二、第一步:完善 provider_paths.xml
配置
首先在 res/xml/
目录下创建 / 修改 provider_paths.xml
,必须包含 Android 10+ 分区存储下的共享路径(如应用私有目录、媒体库目录),否则高版本系统会提示 "无法访问文件"。
xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 1. 应用私有目录(Android 10+ 推荐,无需存储权限) -->
<!-- 私有缓存目录:/data/data/com.nyw.anmoapp/cache/ -->
<cache-path
name="cache_files"
path="." />
<!-- 私有文件目录:/data/data/com.nyw.anmoapp/files/ -->
<files-path
name="files_files"
path="." />
<!-- 私有下载目录:/data/data/com.nyw.anmoapp/files/Download/ -->
<external-files-path
name="external_files_download"
path="Download" />
<!-- 2. 外部存储公共目录(需存储权限,适配旧版本) -->
<!-- 外部存储根目录:/storage/emulated/0/ -->
<external-path
name="external_root"
path="." />
<!-- 外部存储下载目录:/storage/emulated/0/Download/ -->
<external-path
name="external_download"
path="Download" />
<!-- 3. 媒体库目录(Android 10+ 访问媒体文件) -->
<external-media-path
name="external_media"
path="." />
</paths>
配置说明:
路径标签 | 对应实际路径 | 用途 |
---|---|---|
cache-path |
应用私有缓存目录 | 临时文件(如缓存图片),无需权限 |
files-path |
应用私有文件目录 | 持久化文件,无需权限 |
external-files-path |
应用外部私有目录(如 Download) | Android 10+ 推荐,无需权限 |
external-path |
外部存储公共目录 | 旧版本(Android 9-)访问公共文件 |
external-media-path |
媒体库目录 | Android 10+ 访问图片 / 视频 / 音频 |
三、第二步:修复 AndroidManifest.xml
中的 FileProvider
配置
你的现有配置中 android:authorities
建议补充包名后缀(避免与其他应用冲突),同时确保 meta-data
的 name
正确(兼容 AndroidX):
xml
<provider
android:name="androidx.core.content.FileProvider"
<!-- 权威名:包名 + ".fileprovider"(必须唯一) -->
android:authorities="com.nyw.anmoapp.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
<!-- 引用上面配置的 provider_paths.xml -->
android:resource="@xml/provider_paths" />
</provider>
关键修改:
android:authorities
:从com.nyw.anmoapp
改为com.nyw.anmoapp.fileprovider
(更规范,避免冲突);- 确保
android:exported="false"
(禁止外部应用直接访问,必须通过权限授予); android:grantUriPermissions="true"
(允许临时授予其他应用访问文件的权限)。
四、第三步:在代码中使用 FileProvider
生成 Uri(核心兼容逻辑)
在需要向其他应用暴露文件的场景(如调用系统相机拍照、打开本地文件、分享文件),必须通过 FileProvider.getUriForFile()
生成 content://
Uri ,替代旧的 file://
Uri。
以下是 3 个常见场景的兼容代码示例:
场景 1:调用系统相机拍照(保存到应用私有目录)
java
运行
/**
* 调用系统相机拍照(适配 Android 7.0+)
*/
private void takePhotoWithCamera() {
// 1. 创建保存照片的文件(存到应用私有缓存目录,无需权限)
File photoFile = new File(getCacheDir(), "temp_photo.jpg");
try {
if (photoFile.exists()) {
photoFile.delete();
}
photoFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
return;
}
// 2. 生成 Uri(Android 7.0+ 用 FileProvider,低版本直接用 file://)
Uri photoUri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 注意:authorities 必须和 Manifest 中配置的一致
photoUri = FileProvider.getUriForFile(
this,
"com.nyw.anmoapp.fileprovider", // 与 Manifest 中的 android:authorities 对应
photoFile
);
// 授予临时权限(允许相机应用访问该 Uri)
grantUriPermission(
"com.android.camera", // 目标应用包名(相机)
photoUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
);
} else {
// Android 7.0 以下直接用 file:// Uri
photoUri = Uri.fromFile(photoFile);
}
// 3. 启动相机
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(intent, 1001); // 1001 为拍照请求码
}
场景 2:打开本地文件(如 PDF、TXT)
java
运行
/**
* 打开本地文件(适配 Android 7.0+)
* @param file 要打开的文件
*/
private void openLocalFile(File file) {
if (!file.exists()) {
Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
return;
}
// 1. 生成 Uri
Uri fileUri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
fileUri = FileProvider.getUriForFile(
this,
"com.nyw.anmoapp.fileprovider",
file
);
} else {
fileUri = Uri.fromFile(file);
}
// 2. 配置文件打开意图
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(fileUri, getMimeType(file.getAbsolutePath())); // 获取文件 MIME 类型
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 授予临时权限(允许目标应用访问该 Uri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 3. 启动应用打开文件
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, "没有找到可打开该文件的应用", Toast.LENGTH_SHORT).show();
}
}
/**
* 获取文件 MIME 类型
*/
private String getMimeType(String filePath) {
String type = null;
String extension = MimeTypeMap.getFileExtensionFromUrl(filePath);
if (extension != null) {
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase());
}
return type;
}
场景 3:配合 HttpUtils
下载文件后打开
在 HttpUtils
的 DownloadProgressListener
中,通过 FileProvider
打开下载的文件:
java
运行
HttpUtils.getInstance().download(
"https://example.com/test.pdf",
"Download",
"test.pdf",
new HttpUtils.DownloadProgressListener() {
@Override
public void onProgress(int progress) {
// 进度更新
}
@Override
public void onSuccess(String filePath) {
// 下载完成,打开文件
File downloadFile = new File(filePath);
openLocalFile(downloadFile); // 调用上面的 openLocalFile 方法
}
@Override
public void onFail(String errorMsg) {
Toast.makeText(MainActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
}
}
);
五、第四步:Android 10+ 特殊兼容(分区存储)
如果你的文件需要保存到外部存储公共目录 (如 /storage/emulated/0/Download/
),在 Android 10+ 中还需要注意:
- 若使用
MediaStore
保存媒体文件(图片 / 视频 / 音频),无需WRITE_EXTERNAL_STORAGE
权限,直接通过ContentResolver
插入即可; - 若保存非媒体文件(如 PDF、TXT),在 Android 10+ 中需通过 SAF(存储访问框架) 让用户选择保存位置,或使用应用私有目录(推荐)。
总结:必须兼容的核心点
provider_paths.xml
必须包含全版本路径:覆盖应用私有目录、外部存储目录、媒体库目录;Manifest
中authorities
必须唯一 :建议用 "包名 +.fileprovider
" 格式;- 代码中必须区分版本生成 Uri :Android 7.0+ 用
FileProvider
,低版本用Uri.fromFile
; - 必须授予临时权限 :通过
grantUriPermission
或Intent.FLAG_GRANT_READ_URI_PERMISSION
允许其他应用访问文件。
这样配置后,你的 FileProvider
就能在 Android 7.0 到 Android 14+ 全版本中正常工作,避免 FileUriExposedException
等兼容性问题。