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 等兼容性问题。

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui