解决FileUriExposedException:Android安全文件共享
2025-03-25 04:49:52
解决 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-path
、files-path
、cache-path
等),并为它们提供清晰、性的名称。
5. 进阶使用技巧与安全建议
-
临时权限和 FLAG_GRANT_PERSISTABLE_URI_PERMISSION :
如果你需要更精细地控制 URI 的权限持续时间,可以使用FLAG_GRANT_PERSISTABLE_URI_PERMISSION
标志。这会要求接收方显式地请求保留权限。一般情况下, 通过 Intent 传递的 URI 权限是临时的(与FLAG_GRANT_READ_URI_PERMISSION
或FLAG_GRANT_WRITE_URI_PERMISSION
一起使用). 只要接收方的 Activity 栈还在,这些权限就有效。 当 Activity 栈销毁时,权限自动失效。
可以用takePersistableUriPermission()
方法把临时权限变成持久权限. 但是很少有这种场景,所以通常用临时的即可。 -
严格控制共享范围 :只分享你确实需要分享的文件,而且在
file_paths.xml
中尽可能精确地指定路径. -
及时回收权限 : 如果是手动调用
grantUriPermission
授予的权限,在不需要的时候,可以通过revokeUriPermission
来回收。 不过通过Intent
传递的,基本不需要手动回收。 -
不要滥用外部存储: :尽量使用内部存储或应用私有目录,只有必要时才将文件放在公共的外部存储。
通过这些修改,你应该可以解决 FileUriExposedException
问题,并安全地在你的应用和其他应用之间共享文件了. 记住, 安全永远是第一位的!