Android ACTION_SEND 调用 Gmail 后 PDF 附件丢失,如何排查?
结论:现有信息不足以确认是 Gmail 的已知缺陷
你的代码已经具备分享 PDF 的主要条件:FileProvider、content:// URI、application/pdf、附件流和读取授权。仅凭“撰写界面显示附件,发送后进入草稿箱且附件消失”,无法确认是 Gmail 缺陷,也无法认定 URI 权限已经过期。
接下来可以分别检查文件是否可读、接收端是否持续拥有读取权限,以及 Gmail 能否正常发送邮件。撰写界面显示附件名称,不代表附件内容已经被完整读取或保存。
为什么已有措施仍不能确定问题所在?
Android 官方建议使用 ACTION_SEND,将附件 URI 放入 Intent.EXTRA_STREAM,并向接收应用授予 URI 访问权限。你的实现符合这一基本做法,收件人使用 String[] 也符合 Gmail 的要求。Android 数据分享说明
以下操作各有用途:
flush()和sync()处理文件写入,不会延长接收应用的 URI 访问权限。- 唯一文件名可以减少复用同一文件带来的干扰,但不能证明 Gmail 已经读取文件。
ShareCompat.IntentBuilder帮助构造分享 Intent,不能保证邮件客户端完成附件导入和发送。<queries>用于包可见性查询,不会授予 PDF 读取权限,也不会修复已进入撰写界面后出现的附件问题。Android 包可见性说明
第一步:检查实际文件和 FileProvider 映射
当前保存位置是:
getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)/EmailTests/文件名.pdf
它与配置中的 Documents/EmailTests 相对应。如果改用 getCacheDir() 或 getExternalCacheDir(),必须同步配置对应的 <cache-path> 或 <external-cache-path>。不同存储目录不能直接沿用同一个 <external-files-path>。FileProvider 路径说明
建议先保留当前目录,增加以下诊断信息:
- 检查
getExternalFilesDir(...)是否返回null。 - 检查
mkdirs()的结果,目录创建失败时记录错误。 - 记录文件大小、生成时间和对应 URI,确认文件大小大于零。
- 通过该 URI 读取数据,并用 PDF 阅读器验证文档能否打开。
- 在复现期间保留文件,检查是否有主动删除附件或撤销授权的代码。
可以在生成 URI 后执行以下检查;示例中的 InputStream、IOException 和 Log 需要相应导入:
Log.d("PdfShare", "size=" + pdfFile.length());
Log.d("PdfShare", "type=" + getContentResolver().getType(contentUri));
try (InputStream input = getContentResolver().openInputStream(contentUri)) {
if (input == null) {
throw new IOException("Cannot open PDF URI");
}
byte[] buffer = new byte[8192];
long total = 0;
int count;
while ((count = input.read(buffer)) != -1) {
total += count;
}
Log.d("PdfShare", "readBytes=" + total);
} catch (IOException | SecurityException e) {
Log.e("PdfShare", "PDF URI read failed", e);
return;
}
这段代码只能验证发送应用自身能否通过 URI 读取完整数据,无法证明 Gmail 已获得访问权限。原代码中的 catch (IOException e) { return null; } 也应增加异常日志,以免遗漏文件生成失败的线索。
第二步:保留最小分享 Intent 进行对照
可以暂时用下面的代码替代 ShareCompat.IntentBuilder 进行对照,减少封装层带来的变量。原来的 Builder 用法不一定有错。
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.setType("application/pdf");
sendIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{RECIPIENT});
sendIntent.putExtra(Intent.EXTRA_SUBJECT, SUBJECT);
sendIntent.putExtra(Intent.EXTRA_TEXT, BODY);
sendIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
sendIntent.setClipData(
ClipData.newRawUri("PDF Attachment", contentUri));
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
startActivity(Intent.createChooser(
sendIntent, "Send Email with PDF..."));
} catch (android.content.ActivityNotFoundException e) {
Log.e("PdfShare", "No compatible app found", e);
}
保持 MIME type 为 application/pdf。当前没有证据表明改为 text/plain、*/* 或 ACTION_SENDTO 能解决附件丢失问题。
第三步:验证是否涉及授权有效期
通过 Intent 授予的 URI 权限通常在接收 Activity 所在任务栈的生命周期结束时移除,授权也会扩展到接收应用的其他组件。因此,Gmail 转入后台处理本身不足以证明它失去了权限,发送应用进入 onPause() 也不等于接收端授权立即失效。FileProvider 授权说明
如果问题集中发生在关闭撰写界面、稍后重开草稿或延迟发送时,可以做一次定向授权对照。以下代码仅用于已确认安装并准备直接打开 Gmail 的测试环境,放在前述 startActivity(...) 之前:
String gmailPackage = "com.google.android.gm";
grantUriPermission(
gmailPackage,
contentUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION);
sendIntent.setPackage(gmailPackage);
这种按包名授予的权限可持续到主动撤销或设备重启,适合与 Intent 临时授权进行对照。它仍然要求源文件一直存在。AndroidX FileProvider 授权实现说明
如果这样做能稳定改善现象,可以继续调查授权生命周期或接收端的附件读取流程,但还不能据此认定 Gmail 存在内部缺陷。如果没有改善,则应继续排查文件读取和客户端发送流程。
不要在刚启动 Gmail、发送应用进入 onPause() 或用户刚返回应用时立即撤销授权或删除文件。正式采用显式授权时,需要管理授权记录和文件清理时机;分享界面关闭不能作为邮件已经发送成功的凭据。
第四步:用对照测试区分附件问题和发送问题
建议使用同一个 Gmail 账号、同一份 PDF,依次测试:
| 测试 | 用途 |
|---|---|
| 在 Gmail 内手动撰写纯文本邮件并发送 | 检查基础发送能力 |
| 将生成的 PDF 导出后,在 Gmail 内手动添加并发送 | 检查文件及手动附件流程 |
| 通过应用分享,立即发送 | 检查分享附件的即时处理 |
| 通过应用分享,保存草稿后重开并发送 | 检查草稿恢复过程 |
| 将相同 PDF 分享给另一个邮件客户端 | 判断问题是否集中在 Gmail |
如果手动添加成功、通过分享失败,URI 访问或分享接收流程就更值得怀疑;如果其他客户端成功、只有 Gmail 失败,可以进一步排查特定 Gmail 版本、设备或账号环境。这些都是定位线索,不能单独证明根因。
复现时记录 Android 版本、Gmail 版本、设备型号,以及点击发送前后的 Logcat。重点查找与该 URI 或 provider authority 相关的 SecurityException、Permission Denial、FileNotFoundException 和 I/O 错误。Gmail 自身日志可能有限,没有日志也不能证明读取成功。
缺少版本信息和读取失败的证据时,建议保留当前的标准分享方式,用上述对照测试缩小范围。如果业务必须支持长期保留草稿,应提供导出 PDF 的兜底入口,让用户在邮件客户端内手动添加,并针对实际支持的客户端验证行为。
备注:内容仅供参考。
