PAYATHON 2026

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 路径说明

建议先保留当前目录,增加以下诊断信息:

  1. 检查 getExternalFilesDir(...) 是否返回 null。
  2. 检查 mkdirs() 的结果,目录创建失败时记录错误。
  3. 记录文件大小、生成时间和对应 URI,确认文件大小大于零。
  4. 通过该 URI 读取数据,并用 PDF 阅读器验证文档能否打开。
  5. 在复现期间保留文件,检查是否有主动删除附件或撤销授权的代码。

可以在生成 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 的兜底入口,让用户在邮件客户端内手动添加,并针对实际支持的客户端验证行为。

备注:内容仅供参考。