返回

解决FileUriExposedException:Android安全文件共享

Android

解决 FileUriExposedException:当 Uri 通过 ClipData.Item.getUri() 暴露给其他应用

在使用 Android 相机和蓝牙功能时,你可能会遇到 FileUriExposedException: file:///sdcard/Download/example.txt exposed beyond app through ClipData.Item.getUri() 这个错误。 简单说,就是你的应用试图将一个 file:// 类型的 Uri 通过 Intent 传递给另一个应用,而 Android 7.0 (API 级别 24) 及更高版本出于安全考虑,禁止了这种做法。

问题根源

从 Android 7.0 开始,直接使用 file:// 形式的 Uri 暴露给其他应用会被视为不安全。这是因为 file:// Uri 直接指向了你应用的私有文件,其他应用可能没有权限访问,或者可能会滥用这些文件。 为了解决这个问题,Android 引入了 FileProvider

解决方案

核心思路是用 FileProvider 生成的 content:// 类型的 Uri 来替代 file:// 类型的 Uri。FileProvider 能够安全地分享你的应用私有文件,它会为每个文件生成一个临时的访问令牌。

下面是详细的步骤,包括相机和蓝牙文件共享:

1. 修改相机部分代码 (CameraActivity.class)

这部分你已经使用了 FileProvider, 写法没问题, 但有些细节可以优化:

private void sendTakePhotoIntent() {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    // 检查是否有相机应用可以处理这个 Intent
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        File photoFile = null; //把 photoFile 提到外面定义
        try {
            photoFile = createImageFile();
        } catch (IOException ex) {
           //处理创建文件失败
            ex.printStackTrace(); // 打印错误信息,方便调试
            Toast.makeText(this,"创建图片文件失败!",Toast.LENGTH_SHORT).show();
            return;
        }

        if (photoFile != null) {
            // 使用 FileProvider 获取 content Uri
            Uri photoUri = FileProvider.getUriForFile(this, getPackageName(), photoFile);
             // 将 Uri 放入 Intent
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);

            // 授予读写权限(重要!许多教程会漏掉这一步!)
              List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
            for (ResolveInfo resolveInfo : resInfoList) {
                String packageName = resolveInfo.activityInfo.packageName;
                grantUriPermission(packageName, photoUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
            }
            
            startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
        }
    }
}

改进说明:

  • 明确错误处理 : 在创建文件失败时,除了捕获异常,添加了 Toast 提示和打印错误信息,方便调试。
  • 授予 URI 权限 : 添加了 grantUriPermission 代码, 确保相机应用有写入你提供的 Uri 的权限. 这是非常重要的一步,否则即使使用了 FileProvider, 对方应用也可能无法正确写入文件.

2. 修改蓝牙部分代码 (Bluetooth.class)

你需要用 FileProvider 来获取文件的 content:// Uri, 而不是 Uri.fromFile()

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (resultCode == DISCOVER_DURATION && requestCode == REQUEST_BLU) {
        Intent i = new Intent();
        i.setAction(Intent.ACTION_SEND);
        i.setType("*/*"); // 可以根据实际文件类型更精确地设置

        File file = new File(exist); // 'exist' 应该是一个有效的路径字符串

        // 使用 FileProvider 获取 content Uri
        Uri fileUri = FileProvider.getUriForFile(this, getPackageName(), file);

         // 使用 content Uri
        i.putExtra(Intent.EXTRA_STREAM, fileUri);

          // 授予读取权限
        i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

        // 查找蓝牙应用
        PackageManager pm = getPackageManager();
        List<ResolveInfo> list = pm.queryIntentActivities(i, 0);
        if (list.size() > 0) {
            String packageName = null;
            String className = null;
            boolean found = false;

            for (ResolveInfo info : list) {
                packageName = info.activityInfo.packageName;
                if (packageName.equals("com.android.bluetooth")) {
                    className = info.activityInfo.name;
                    found = true;
                    break;
                }
            }

            if (!found) {
                Toast.makeText(this, "未找到蓝牙应用", Toast.LENGTH_LONG).show();
            } else {
                i.setClassName(packageName, className);
                startActivity(i);
            }
        } else {
            Toast.makeText(this, "没有支持发送的应用", Toast.LENGTH_LONG).show(); // 更友好的提示
        }
    } else {
        Toast.makeText(this, "蓝牙操作被取消", Toast.LENGTH_LONG).show();
    }
}

改进说明:

  • Uri.fromFile(file) 改为 FileProvider.getUriForFile(this, getPackageName(), file),生成 content:// URI.
  • 增加了 i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);, 授予接收应用读取 URI 的权限。
  • 错误提示更加清晰。
  • setType("*/*");改为一个根据实际情况改变的类型,比如"image/jpeg" or "text/plain" 会更有效。

3. 修改 AndroidManifest.xml

你提供的 provider 定义基本正确,但 android:authorities 应该与你在代码中使用的相匹配,通常是你的应用的包名。file_paths.xml 的配置也要检查。

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${applicationId}"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

改进说明:

  • android:authorities 设置为 ${applicationId}. 这是一种最佳实践,它会自动使用你的应用的包名。不用硬编码包名,更不容易出错,也更方便多模块项目。

4. 修改 res/xml/file_paths.xml

file_paths.xml 定义了哪些目录可以被分享。

<paths>
    <external-path name="my_images" path="Android/data/你的包名/files/Pictures" />
    <external-files-path name="my_pictures" path="Pictures" />
    <external-path name="my_downloads" path="Download" />  <!-- 允许分享 Download 目录 -->
    <cache-path name="my_cache" path="." />   <!-- 允许分享缓存目录下的文件 -->
     <files-path name="my_private_files" path="." />
</paths>

改进说明:

  • <external-path> 指向的是外部存储的公共目录,但很多应用的数据是存储在外部存储的应用私有目录下.
  • 增加了 <external-files-path name="my_pictures" path="Pictures" />, 用于外部存储的应用私有 Pictures 目录(如果需要的话). 你可以通过 getExternalFilesDir(Environment.DIRECTORY_PICTURES) 来获取这个目录.
  • 添加 <external-path name="my_downloads" path="Download" />, 因为你的错误信息中显示你在尝试分享 Download 目录下的文件.
  • <cache-path> 可以用来分享应用缓存目录 (getCacheDir()) 中的文件。
  • <files-path> 共享应用内部存储 getFilesDir()下的文件
  • name 的取值很重要FileProvider.getUriForFile() 会根据你指定的 name 来构造 content URI. 在需要从 URI 获取原始路径时(比如你自己处理文件时,但不要暴露给其他应用),这个 name 就起作用了.
  • 路径可以重叠 :你可以在多个 <path> 元素中使用相同的路径,但是具有不同的 name 属性.
  • 考虑你想共享哪些类型的路径(external-pathfiles-pathcache-path等),并为它们提供清晰、性的名称。

5. 进阶使用技巧与安全建议

  • 临时权限和 FLAG_GRANT_PERSISTABLE_URI_PERMISSION :
    如果你需要更精细地控制 URI 的权限持续时间,可以使用FLAG_GRANT_PERSISTABLE_URI_PERMISSION标志。这会要求接收方显式地请求保留权限。一般情况下, 通过 Intent 传递的 URI 权限是临时的(与FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION 一起使用). 只要接收方的 Activity 栈还在,这些权限就有效。 当 Activity 栈销毁时,权限自动失效。
    可以用 takePersistableUriPermission() 方法把临时权限变成持久权限. 但是很少有这种场景,所以通常用临时的即可。

  • 严格控制共享范围 :只分享你确实需要分享的文件,而且在 file_paths.xml 中尽可能精确地指定路径.

  • 及时回收权限 : 如果是手动调用grantUriPermission授予的权限,在不需要的时候,可以通过revokeUriPermission来回收。 不过通过Intent传递的,基本不需要手动回收。

  • 不要滥用外部存储: :尽量使用内部存储或应用私有目录,只有必要时才将文件放在公共的外部存储。

通过这些修改,你应该可以解决 FileUriExposedException 问题,并安全地在你的应用和其他应用之间共享文件了. 记住, 安全永远是第一位的!