Android 学习笔记 D19|系统分享与 FileProvider:别人怎样读到你的文件
发布摘要:先分享文章标题和链接,再用最小 FileProvider 配置分享导出的文本文件,理解 Intent、URI 和临时授权。标签:Intent、FileProvider、Android、文件分享。
文章阅读页增加分享按钮,第一版只需要把标题和链接交给系统分享面板。下一步如果把收藏列表导出成文本文件,代码却不能只是把 cacheDir 路径塞进去:另一个应用没有权限访问我们的私有目录。两种分享看起来都是"发送",实际传递的数据和授权完全不同。
本文对应 D19;书中第 3 章 Intent、第 8 章 ContentProvider 和第 9 章多媒体文件访问可以一起回读。代码是未编译运行的教学片段,需结合实际包名、Manifest 和 AndroidX Core 依赖接入。
一、文本分享先确认三个字段
ACTION_SEND 表达发送一个内容,MIME type 表达内容类型,EXTRA_TEXT 承载文本。标题和链接合成一段文字即可,不需要通讯录权限,也不需要直接集成某个社交平台 SDK。由用户在系统面板中选择目标应用。系统分享数据
kotlin
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
fun shareArticle(activity: Activity, title: String, url: String): Boolean {
val payload = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, "$title\n$url")
}
return try {
activity.startActivity(Intent.createChooser(payload, "分享文章"))
true
} catch (_: ActivityNotFoundException) {
false
}
}
该函数要求从 Activity 的用户点击事件调用。返回 true 仅表示启动流程没有抛出这里处理的异常,不表示对方收到内容,更不表示用户最终发送。分享面板被关闭也是正常选择,不应该因此把文章标成"已分享成功"。
第一步先使用受控标题和 URL 检查输出;第二步接入实际文章;第三步检查空标题、长标题和取消;最后才做文件分享。先验证数据内容,再引入授权,排错会容易很多。
二、私有文件通过受控 URI 暴露
FileProvider 是 ContentProvider 的常用实现,负责把允许的文件路径映射为 content:// URI。URI 不是物理路径,也不自动代表接收方获得读权限。配置中同时需要限制可分享目录和授予本次读取权限。FileProvider 文件分享
Manifest 的 application 内声明如下。${applicationId} 为 Android Gradle 插件替换的应用标识;运行时代码使用同一个 authority。paths 文件放在 res/xml/share_paths.xml。
xml
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/share_paths" />
</provider>
xml
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="exports" path="exports/" />
</paths>
下面函数同样从 Activity 调用,file 必须是已完成写入、位于 cacheDir/exports 的文件。需要 android.content.ClipData、androidx.core.content.FileProvider 和 java.io.File 导入,并复用前面 Intent 相关导入。
kotlin
fun shareExport(activity: Activity, file: File): Boolean {
val uri = FileProvider.getUriForFile(
activity, "${activity.packageName}.fileprovider", file
)
val payload = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri("收藏导出", uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
return try {
activity.startActivity(Intent.createChooser(payload, "分享导出文件"))
true
} catch (_: ActivityNotFoundException) {
false
}
}
目录权限配置错误可能让 getUriForFile 抛出异常,应作为开发配置错误排查;不要把它和"没有分享目标"混在同一句提示里。导出写入放到后台线程,完成后才在主线程打开分享界面。多个导出使用独立文件名,避免用户还在读取上一份文件时被新导出覆盖。
三、为什么不配置整个根目录
把路径写成宽泛的根目录确实可能让示例更容易运行,但它使允许分享的范围远大于功能需要。本文只允许 exports 子目录,只授予读权限,文件内容也只包含用户选择导出的收藏数据。分享一份文本不需要连数据库、配置文件一起暴露。
缓存文件的清理也有时序:发起分享后立即删除,目标应用可能还没读到;永不清理又会持续积累。练习采用带时间戳的导出文件,保留合理窗口并在后续启动清理过期文件,不能宣称存在一个所有目标应用都保证读完的固定秒数。
如果另一个方向是用户从系统文件选择器导入文档,是否能长期读取取决于返回的授权和是否取得持久 URI 权限。保存 URI 字符串不等于保存权限。FileProvider 临时分享和 Storage Access Framework 的持久授权应分开理解,后者将在 B11 展开。
四、故障实验与预期
第一个实验只分享文本,记录目标应用看到的换行、标题和链接。预期保留可读内容;不同目标应用的预览形式不由我们控制。取消后返回详情,预期收藏、阅读位置和主题不变。
第二个实验在本地调试分支去掉读取授权,并用另一个应用读取导出文件。预期可能出现权限失败,恢复授权后重测。不能因为某个目标应用恰好能读取,就推断所有接收者无需授权。
第三个实验把文件放到未配置的子目录,预期在生成 URI 时暴露配置问题。修复应是把真正需要的导出移到指定目录,而不是扩大到整个存储根目录。最后在文件生成失败时点击分享,预期显示导出失败,不启动一个指向不存在内容的 URI。
五、原创面试问答与追问
题库入口可用显式与隐式 Intent和四大组件。下面是自拟项目问答。
问:为什么不能把私有文件绝对路径直接给另一个应用? 答:路径只是位置描述,接收方通常没有该目录访问权。追问:content URI 本身够吗?还需要对应提供者允许访问和必要授权。
问:系统分享面板打开就算分享成功吗? 答:只能说明进入了选择流程。追问:如何描述项目能力?说实现了系统分享,不声称验证了某平台实际送达。
问:为什么只加读权限? 答:接收方只需读取导出结果,不需要修改私有文件。追问:分享后立刻删除是否更安全?可能破坏正常读取,应结合导出文件保留和清理策略处理。
六、当天练习与验收
二十分钟复习 Intent 字段,四十分钟读 FileProvider 配置,一百五十分钟实现文本与文件分享,三十分钟故障实验,一小时说明组件职责。验收交付是文本分享记录、目录与 authority 对照、授权缺失实验,以及取消分享后的界面状态。
独立练习把导出格式换为 JSON,同时正确调整扩展名和 MIME type。最终需要能解释"生成文件---映射 URI---授予读取---目标读取"的路径,而不是仅展示一个分享按钮。