直接回答
通知动作是一种能力:允许应用自有组件执行某个操作,不是通知发布时下载状态的永久记录。系统交给通知服务的令牌是 PendingIntent。在同一创建者和操作类型内,Android 根据 requestCode、Intent.filterEquals() 身份和相关 descriptor flags 匹配令牌。不能把身份简化为 Intent 加 requestCode;one-shot 或可变性等 flags 也可能参与匹配。filterEquals() 不比较 extras,所以只换 attempt ID extra 不会建立独立令牌。 这会影响并行尝试、重试替换和延迟更新。假设新旧取消按钮使用相同操作、requestCode、显式接收器、filter-equal Intent 和相关 flags,只改变 extra。创建方可以取回原令牌;使用 FLAG_UPDATE_CURRENT 时会更新 extras,用户再点旧卡可能收到新 attempt ID。若接收器只相信 extra,旧动作可能取消用户没选的新任务;没有更新标志时也可能保留旧 payload。两种情况都不能区分尝试。 要分别确认令牌身份、payload 和当前授权。稳定模型 ID 标识工件修订,但重试需要独立 attempt 身份。应改变 PendingIntent 实际匹配的值,例如经过冲突检查的 attempt requestCode 或不同且不含敏感信息的 data URI;同时保留相关 descriptor flags,并明确操作类型、显式组件、目标包和 action。不要依赖 Java hashCode 保证唯一,也不要把秘密放入身份字段。派生数字须在其生命周期内处理冲突。 不可变性和唯一性不同。无需接收方填入字段时,FLAG_IMMUTABLE 限制 PendingIntent 接收者修改;它不创建 attempt 专属令牌、不把 extras 纳入 filterEquals(),也不证明下载仍活动。Android 允许创建方以 FLAG_UPDATE_CURRENT 更新自己的 immutable PendingIntent。不可变性是接收方修改边界,不是任务身份或授权。应按动作契约选择可变性,而非用它携带本应参与身份匹配的 attempt ID。 除了令牌,还须把执行绑定到当前任务记录。应用自有接收器或服务收到命令时,由传输所有者在同一串行或条件边界内核对 attempt ID、工件修订、work ID 和可取消状态,并取消捕获的对应 work handle。不能先校验某次尝试、再按工件全局“current”指针重新查找取消对象,否则它可能已指向新重试;状态写回也须以同一 attempt 为条件。缺失、已替代或终结的尝试应过期无操作。幂等性有助于重复点击,但不能单独消除替代竞态。Intent ID 是待校验的选择器,不是授权;身份字段和通知 payload 不要放秘密。 分别测试令牌匹配和工作授权:同一创建者、操作类型、requestCode、filter-equal Intent 与相关 descriptor flags 仅改变 extras,确认并非独立身份;再改变身份输入并检查通知替换后的令牌。结合 FLAG_UPDATE_CURRENT 与 immutable descriptor 验证创建者更新和接收方填入的区别。让旧通知点击与新重试创建竞态,确认所有者只在同一串行或条件边界内取消捕获的旧尝试,状态写回也按该 attempt 条件执行,绝不重新读取全局 current 指针。还覆盖重复点击和任务完成。 产品声明要保持边界。Android 指南说明持久工作有自己的生命周期规则,仓库事实描述下载行为;二者都不证明 Cove 当前发布了模型下载通知或 PendingIntent 取消动作。本文仅适用于确实将通知控件连接到下载尝试的应用,不声称 Cove 有此功能或缺陷,也不保证后台完成,不替代验证传输所有者的取消语义。
旧下载通知误取消了重试任务
应用在上一模型传输失败后开始重试,旧通知仍在屏幕上。新旧取消按钮使用同一操作、requestCode、显式组件和 filter-equal Intent,只改变 attempt ID extra。用户此时才点旧通知。
决策
为每次尝试创建不同的 PendingIntent 身份,并在执行取消前用权威任务记录核对 attempt ID 和可取消状态。
预期结果
旧通知被识别为过期并无操作,新尝试继续运行;当前尝试自己的取消按钮仍可正常工作。
需要收集的证据
只有将以下证据与同一模型文件和测试运行关联起来,结论才可复核:
- ANDROID-BG-01:持久工作有自身生命周期规则,不能仅凭通知推断任务会持续运行。
- COVE-DOWNLOAD-01:当前 ModelDownloader 事实描述传输、临时文件、进度和取消相关边界,不证明 Cove 已实现下载通知 PendingIntent。
- ANDROID-PENDINGINTENT-IDENTITY-2026-10:Android PendingIntent token matching、extras 更新、不可变性和动作安全边界。
- Android PendingIntent 与 Intent.filterEquals 官方 API 文档定义令牌匹配、额外数据和标志语义。
收到完整字节流还不够;最终落盘、完整性、运行时加载和恢复路径也必须验证。
实施流程
- 定义操作与尝试身份. 将不可变的工件修订与每次传输尝试区分。明确应用创建者、PendingIntent 操作类型、接收器或服务、requestCode 分配策略、显式组件、目标包、action、data 和相关 flags。
- 确定实际令牌匹配键. 检查 PendingIntent 创建者与操作 API,再核对 requestCode 和 Intent.filterEquals 文档定义的身份字段。确认只换 extra 不会生成不同令牌。
- 为不同尝试提供身份字段. 使用经过冲突检查的 attempt requestCode,或唯一且不含敏感信息的 Intent data URI。不要单独依赖 Java hashCode,也不要将签名 URL 或私人内容放入身份字段。
- 按真实用途选择可变性. 接收器无需填入 Intent 时使用 immutable。FLAG_IMMUTABLE 仅限制接收方修改;它不提供 attempt 唯一性,创建方仍可能用 FLAG_UPDATE_CURRENT 更新 extras。
- 在所有者边界校验并取消. 由传输所有者在同一串行或条件边界核对 attempt、修订、work ID 和状态,并取消捕获的对应 work handle。校验后不要重新读取工件全局 current 指针;状态写入以同一 attempt 为条件。幂等性本身不能防止替代竞态。
- 覆盖旧通知和重复动作. 测试同时下载多个模型、同一模型重试、替换通知后点击旧卡、重复点击、已完成任务和取消竞态。断言只有身份匹配的活动尝试会变化,取消命令确实到达其执行者。
让每个阶段的状态都可观察。失败信息应指出阶段、模型、运行时和恢复动作,同时避免记录用户私密内容。
验收标准
- 同一创建者和操作类型下,若 requestCode、filterEquals 身份及相关 descriptor flags 相同,仅 extras 不同的动作不视为独立 PendingIntent 身份。
- 在明确的创建者、操作与 descriptor flag 范围内,每次可操作下载尝试都有无冲突身份,且不同于模型工件 ID。
- 传输所有者在串行或条件边界校验并取消捕获的 attempt;状态写回以该 attempt 为条件,不重新读取全局 current 指针。
- 可变性标志与 attempt 唯一性分开论证,不能用 immutable 替代当前任务授权检查。
决策对照表
| 维度 | 选择或证据 | 解读方式 |
|---|---|---|
| 固定 requestCode,attempt ID 只放 extras | 仅在有意让令牌代表唯一当前操作时使用。 | extras 不区分 filter-equal Intent;FLAG_UPDATE_CURRENT 会替换令牌持有者看到的 payload。 |
| attempt 专属 requestCode | 当并发或已替代操作必须区分时,使用唯一分配的数字代码。 | 分配逻辑必须处理冲突和生命周期复用;hash 值本身不保证唯一。 |
| 不同 Intent data 身份 | 为每次尝试设置不同且不含敏感内容的数据 URI 或其他文档字段。 | filterEquals() 可据此区分令牌,但接收器仍须向当前工作状态校验 attempt。 |
| FLAG_IMMUTABLE | 接收方无需填入字段时,阻止 PendingIntent 接收方修改 Intent。 | 不能把 extras 纳入匹配,也不能阻止创建方用 FLAG_UPDATE_CURRENT 更新令牌。 |
需要避免的失败模式
- 把 attempt ID 放入 extras 就当作已创建独立 PendingIntent。
- 把 FLAG_IMMUTABLE 当成唯一性或当前任务授权检查。
- 旧通知仍保留时,用 FLAG_UPDATE_CURRENT 覆盖同一令牌 extras,却不验证 attempt 是否仍是活动任务。
- 用未做冲突检查的 hashCode 充当永不重复的 requestCode。
还要避免静默回退、版本未固定、缺少取消处理,以及把不同配置的结果混在一起下结论。失败运行也应与成功记录一并保存。
最小复现记录
| 层级 | 记录内容 |
|---|---|
| 设备 | 厂商、型号、芯片、内存档位、操作系统版本 |
| 软件 | 应用版本和 Git 提交号 |
| 模型 | 系列、变体、版本、格式、文件长度、摘要、量化方式 |
| 运行时 | 名称、版本、请求后端及实际后端证据 |
| 工作负载 | 样例版本、输入摘要、提示词摘要、输出策略 |
| 结果 | 完成、失败、取消、回退及保护隐私的诊断信息 |
发布检查表
- ☐ 主要搜索问题已回答,且没有未经支持的数字。
- ☐ 所有模型文件和运行时版本均已固定。
- ☐ 代表性任务和失败处理策略均有说明。
- ☐ 生命周期、取消、清理和回退均已测试。
- ☐ 用户内容与网络边界均有说明。
- ☐ 结论只适用于被记录的配置。
- ☐ 新增测试结果时链接到原始方法或证据。
来源与相关证据
- Android persistent background work
- Android Developers:PendingIntent API 参考
- Android Developers:Intent.filterEquals API 参考
- Android Developers:PendingIntent 安全风险
- Android Developers:通知动作按钮
中文部署与排错专题见。奇连 AI 端侧专题。
常见问题
增加一个新的 attempt ID extra 会创建新的 PendingIntent 吗?
不会。Android 按操作与 Intent 身份规则匹配;Intent.filterEquals() 不比较 extras。多个活动尝试需要不同令牌时,要改变文档定义的身份字段或 requestCode。
FLAG_IMMUTABLE 会阻止 FLAG_UPDATE_CURRENT 替换 extras 吗?
不会。Android 文档说明创建方仍可更新自己的 immutable PendingIntent。不可变性限制接收方填入字段,不建立唯一身份,也不授权取消任务。
唯一 requestCode 足以信任取消动作吗?
只有在该操作范围内确认分配无冲突时,它才区分令牌。所有者还须在同一串行或条件边界校验并取消捕获的 attempt,并按该尝试条件写回状态。
旧通知动作是否应取消最新重试?
不应如此。过期动作应被识别为无操作。每次重试拥有自己的 attempt 身份;只有匹配当前活动且可取消的任务记录时才执行取消。