自定义通知铃声没响,先检查的不是通知权限 📅 发布时间:2026/8/24 13:40:02 👁 浏览次数: 我第一次给通知加自定义铃声遇到的现象很直接通知没有按预期带上声音。于是我立刻去看通知权限甚至准备重复申请授权。后来才发现这次问题发生在更前面。通知还没走到权限那一关给NotificationRequest.sound的值就不合格。这个问题很容易被“铃声没响”四个字带偏。它听起来像通知服务或系统设置的问题实际可能是文件没有准备好、文件 URI 没生成、URI 前面漏了uri::或者路径里混进了不该出现的相对跳转。声音是最后的体验前面却有一条很长的文件链路。末尾不对不一定是末尾出了错。我原先以为只要有一个 WAV 文件就行工程资源里确实有article11_notice.wav。我曾经天真地以为资源存在拿它的路径塞给 sound通知自然就会播放。可 rawfile 资源不是一个可以直接交给通知请求的稳定文件 URI。当前页面会把资源读出来写入 EL1 的应用文件目录核对写入前后的字节数再调用fileUri.getUriFromPath()得到文件 URI最后才拼出以uri::开头的 sound 值。否是否是resources/rawfile/article11_notice.wavgetRawFileContent 读取字节写入 EL1 files 目录写入字节数和目标大小是否一致文件准备失败 不进入发布fileUri.getUriFromPath拼接 uri:: fileUrisound 是否符合格式发布前校验失败才允许进入通知权限和 publish这条链路让我改掉一个习惯不要拿文件名替代文件状态。资源名写在工程里只说明打包时有这个文件它不说明运行时已经成功复制到可用目录也不说明最终的sound值符合接口期望。我先看的第一个状态是文件而不是权限页面把文件阶段单独放在fileState。刚进入时它是“等待准备 EL1 文件”开始操作时它会显示正在读 rawfile完成后才变成“EL1 文件已准备并完成大小核对”。这比一个“准备成功”的绿色标签多了几层但每一层都有用。const sourceBytes await getContext(this).resourceManager.getRawFileContent(this.rawFileName); const targetFile fileIo.openSync( sandboxPath, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC ); const bytesWritten fileIo.writeSync(targetFile.fd, rawBuffer); const targetStat await fileIo.stat(sandboxPath); if (bytesWritten ! sourceBytes.byteLength || targetStat.size ! sourceBytes.byteLength) { throw new Error(写入核对失败source${sourceBytes.byteLength}, written${bytesWritten}, target${targetStat.size}); }我喜欢这里的字节数核对因为它把“写入动作已经调用”与“目标文件确实完整”分开了。某些问题只看writeSync没报错并不够。真正写了多少、目标文件有多大才是下一步能否相信这个文件的依据。URI 不是路径字符串换个名字文件写好以后页面不会直接把sandboxPath放进通知请求而是先转成 URI再加上协议前缀。这个顺序不能颠倒。const convertedFileUri fileUri.getUriFromPath(sandboxPath); const sound uri::${convertedFileUri}; if (!this.isValidSound(sound)) { throw new Error(sound URI 不合规${sound}); }校验函数也很直白sound 必须以uri::开头且不能包含../或/..。它不会判断铃声好不好听也不假装验证系统最终是否播放它只做发布前应做的事情拒绝一个明显不合规的输入。以前我会觉得这种校验有点啰嗦。直到遇到“权限是开着的、publish 也没报错、但实际行为不对”的问题才明白把输入先收紧有多省时间。一个不合规的 URI 一旦流进后面排查路径会被迫穿过授权、通知中心和系统音量最后才可能发现根源只是最前面的字符串不对。修复后的排查顺序我现在不会从“声音有没有响”反推所有步骤而是顺着数据的生成路径正着查。通知发布URI 校验EL1 文件页面Rawfile 资源通知发布URI 校验EL1 文件页面Rawfile 资源alt[URI 合规][URI 不合规或文件失败]getRawFileContentsourceBytes写入并 stat 核对target sizepath 转 fileUri 并加 uri::soundValue允许执行权限与 publish 流程错误信息publishState failed这条流程里最重要的一点是“失败要尽早停”。publishNotification()会先看sandboxPath是否还是“尚未准备”再看soundValue是否通过校验。任一项不满足就把发布状态写成 failed并说明“请先成功准备 EL1 文件和 uri:: sound”。它不会在一个已知不合法的输入上继续调用 publish让错误飘到更远的地方。我怎么测试不靠耳朵猜测试自定义铃声时耳朵当然最终要参与但它不应该是第一步。先把文件与 URI 流程走通再把系统可见和听觉效果单独核对。否是否是否是否是进入通知铃声页面点击准备 EL1 文件fileState 是否完成大小核对记录文件读取 写入或 stat 错误sound 是否以 uri:: 开头且无相对跳转记录 URI 校验失败 不进入发布再核对通知授权通知是否启用记录权限阻断调用 publishpublish Promise 是否返回记录发布错误另行核对通知可见与人工听觉最后一步专门放在流程末尾是为了避免一句“没听到”把前面的检查全部冲掉。publish返回说明服务接受了请求通知是否可见、系统是否允许当前提示方式、自定义声音是否被听到都还需要在系统界面和人工听觉里分别确认。它们不是这个页面可以凭字符串自行证明的事实。我会把失败停在哪一层写清楚文件准备阶段失败时页面把fileState写成“EL1 文件准备失败”同时把发布状态置为 failed。这里的 failed 不是对铃声播放结果的判断而是明确表示后面的发布没有合规输入可用。相反文件状态完成大小核对、soundValue也通过格式检查只能证明资源已经被整理成候选请求参数它仍然没有说明通知会在系统界面出现更没有说明人耳已经听到了这段 WAV。我还会刻意看sourceSize与targetSize是否同时有值且相等。只看其中一个很容易误判源资源能读到只说明打包资源存在目标文件有大小只说明目标位置有内容。现有代码比较的是bytesWritten、源字节数和stat读出的目标大小三者不一致就抛错。这让“写入过”与“完整写入”分成了两件可复测的事也给异常信息留下了具体数值。回归时我会先手动执行一次准备记录 URI 和三个大小相关字段再重复执行一次确认TRUNC覆盖后仍能完成同样的核对。随后才进入权限和 publish。这样可以把重复操作下的文件准备问题与系统通知策略分开。即使两次 publish 都返回也只记录为请求返回通知可见性与听觉结果依旧在系统界面和人工观察里另记不能由这轮文件测试代替。这次踩坑留下的三条提醒文件准备失败时我不会继续追铃声prepareSandboxSound()的开始会把当前操作写成准备 EL1 沙箱音频并把文件状态切成读取 rawfile。这个细节看起来只是展示文案实际上给了排查一个很实用的分界点如果页面从未离开等待状态先看自动流程是否执行或按钮是否触发如果进入读取后失败才去看资源读取、目标目录、打开文件和写入环节。不要在第一种情况下就讨论 URI也不要在第二种情况下先跑到系统通知设置里找答案。目标文件使用创建、截断、只写模式打开。重复准备时截断的意义是让本轮文件不携带上轮尾部的残留随后代码关闭文件再用 stat 读取目标大小。关闭与 stat 都不是多余动作它们让写入的结果被再次读取而不是只相信一次写调用返回的数值。若三个字节数不一致错误文本里会带源、写入、目标的数字。我会保留这段数字不把它缩写成“文件错误”因为大小不一致将来可能指向完全不同的读取或落盘问题。路径转换后还要看两个值裸的fileUriValue和加前缀后的soundValue。前者帮助确认getUriFromPath的输出后者才是最终放进请求的参数。两者不该混写成“铃声路径”。如果只把沙箱路径拼到 sound 里表面上仍有一段看似正确的字符串发布前校验却会拒绝它如果 URI 中出现相对回退片段也应在这里止住。这个页面校验的是格式与路径安全边界不负责解析 WAV 编码更不负责替系统判断铃声能否被人听见。我会做两轮文件回归。第一轮从新页面开始准备记录源大小、目标大小、URI 和最后操作第二轮不改资源直接再准备一次确认覆盖后的结果仍一致。若第二轮异常就优先从截断、关闭、目录可写等重复操作条件查而不是重新申请通知授权。两轮都完成后才进入发布流程。这样即使最后听觉观察不同也能先排除“本轮输入根本不是同一个文件”的干扰。权限和声音之间还隔着系统行为文件与 URI 都通过后页面才会核对通知是否启用再决定是否调用 publish。通知已启用说明发布不会被这一层明确阻断却不等于系统当前会以声音方式提醒。系统的通知展示、渠道或模式、设备音量以及人工是否实际听到都是页面无法从soundValue推导出的事情。我会把 publish 返回的时间、通知栏观察和听觉观察分开写哪一项缺失就留在哪一项不用“铃声成功”把它们压成一个结论。一次可复查的记录应包含什么至少保留资源名、源字节、目标字节、最终 sound 字符串、权限状态、发布状态、错误文本和触发时间。这样别人接手时可以先复核输入是否完整再复核流程是否到了服务调用最后才在同一设备上做系统界面和人工听觉检查。记录虽然比一句“没响”长却把每次重试从猜测变成了有起点的动作。我还会排除一个看似合理、其实没有根据的猜测既然资源文件是 WAV问题一定出在音频本身。现有页面读取的是原始字节写入后核对大小再构造 URI它没有对音频内容做解码验证也没有播放这个文件做试听。因此在文件准备阶段通过后我只能说本轮字节和 URI 门禁通过不能说音频格式已经被通知系统接受更不能说声音内容一定会被系统播放。把这个未知点保留着反而能防止把编码、系统策略和文件路径搅在一起。发生异常时页面会将格式化后的错误放回状态。若错误来自资源读取先核对资源名和页面调用时机若来自打开、写入或 stat先看目标路径和大小若来自 URI 规则先看最终字符串。每个错误都应该从最近一个转换步骤往回查别从“铃声没响”直接跳到权限。这样的顺序有个实际好处即使设备此刻静音、通知被折叠文件链路仍能独立验证不会因为末端体验变化而失去价值。在人工听觉复测前我还会先写清观察条件例如设备是否有可用输出、系统声音设置是否允许提示、观察人是否在同一时刻查看。这里不是为了给页面补一份它没有的数据而是避免听觉记录没有上下文。听到或没听到都只是外部观察它应该附在 publish 返回之后不能反过来覆盖页面已经记录的文件、权限和请求状态。还有一条排除顺序我会坚持不要为了验证声音就跳过页面自己的防护。有人可能会临时手拼一个看似正确的 URI 直接调用发布以为这样更快但这样得到的结果无法说明 rawfile 到 EL1 的正式路径是否工作。当前页面准备、校验、授权、发布分层存在正是为了让每一步都有可见状态。复测应沿用正式路径失败才知道是哪个环节需要修而不是绕开环节后得到一个无法归因的现象。文件状态从完成回到失败也需要被当作新一轮事实。重复准备时如果源资源读取异常或写入核对不一致旧的 URI 不能继续被当作本轮可靠输入。我会记录发生变化的时间和错误再从准备重新开始。这避免一个已经过期的绿色状态留在页面上让人误以为后续 publish 用到的是刚刚验证的文件。在交接记录中我会把“文件链路已通过”和“铃声已由人工听到”放在不同栏目。前者可由大小、URI、状态和请求记录复查后者依赖目标设备和当时的声音环境。两者同时存在当然更完整但缺少后者并不推翻前者缺少前者也不能仅凭一次听觉印象替代输入检查。如果需要向别人描述本次结果我会避免只说“铃声正常”或“铃声异常”。更准确的说法是文件复制及 URI 校验通过通知请求已返回系统界面和人工听觉仍待目标设备观察或者说明具体停在读取、写入、校验、权限、请求中的哪一步。这样的表达既不会夸大页面能力也能让下一位排查者直接从正确位置继续。这种分层写法也让失败记录更有用没有声音不再是一句结束语而是一次检查应从哪里开始的提示。回归我会连续运行两轮完整页面流程并在每轮结束保存文件大小、最终 URI、权限、发布状态和时间。两轮的文件准备都通过时才比较 publish 返回是否一致若第二轮在文件阶段改变就停止在那里不用第一轮的 URI 给第二轮背书。随后系统界面与人工听觉观察各自记录时间避免把不同轮次的体验混进同一条结论。Rawfile 存在不等于运行时的 EL1 文件已经准备好。文件路径可用不等于NotificationRequest.sound已经是合规的 URI。铃声没有听到时先查文件和 URI 链路再查权限与系统体验。以前我把自定义铃声看成通知参数里的一小段文字。现在再看它更像一次小型交付资源从哪里来落到哪里字节有没有损坏路径怎样变成 URI输入是否允许进入发布。把这些前置环节说清楚后面的“有没有声音”才值得讨论。本文只依据现有页面的文件准备、URI 校验和发布前门禁撰写。系统通知可见性与实际听觉效果仍需在目标设备上分别复核。