FileProvider 配置必须针对 Android 7.0+(API 24+)做兼容

这个 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 基础,但还需要补充两点:

  1. 完善 provider_paths.xml 配置(指定可共享的文件路径,适配 Android 10+ 分区存储);
  2. 在代码中通过 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-dataname 正确(兼容 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 下载文件后打开

HttpUtilsDownloadProgressListener 中,通过 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+ 中还需要注意:

  1. 若使用 MediaStore 保存媒体文件(图片 / 视频 / 音频),无需 WRITE_EXTERNAL_STORAGE 权限,直接通过 ContentResolver 插入即可;
  2. 若保存非媒体文件(如 PDF、TXT),在 Android 10+ 中需通过 SAF(存储访问框架) 让用户选择保存位置,或使用应用私有目录(推荐)。

总结:必须兼容的核心点

  1. provider_paths.xml 必须包含全版本路径:覆盖应用私有目录、外部存储目录、媒体库目录;
  2. Manifestauthorities 必须唯一 :建议用 "包名 + .fileprovider" 格式;
  3. 代码中必须区分版本生成 Uri :Android 7.0+ 用 FileProvider,低版本用 Uri.fromFile
  4. 必须授予临时权限 :通过 grantUriPermissionIntent.FLAG_GRANT_READ_URI_PERMISSION 允许其他应用访问文件。

这样配置后,你的 FileProvider 就能在 Android 7.0 到 Android 14+ 全版本中正常工作,避免 FileUriExposedException 等兼容性问题。

相关推荐
zh_xuan2 小时前
Android 消息循环机制
android
ajassi20002 小时前
开源 java android app 开发(十五)自定义绘图控件--仪表盘
android·java·开源
jzlhll1233 小时前
deepseek Kotlin Flow 全面详解
android·kotlin·flow
ZHANG13HAO3 小时前
Android 13 完整实现 USB 网卡支持与网络优先级配置(USB>WiFi>4G)
android·网络
梦终剧3 小时前
【Android之路】界面和状态交互
android·交互
孙小二3 小时前
Android主题切换
android
帅锅锅0074 小时前
Android.mk 编辑脚本
android
火柴就是我5 小时前
Android 记录View绘制坐标抖动问题
android
余衫马5 小时前
Ubuntu24.04 安卓模拟器安装指南
android·容器·模拟器