一、Toast 弹了,提醒没响:Android 写 Google 日历的典型断点
如果你正在做 Android 端往 Google 日历写活动的功能,大概率见过这个画面:Toast老老实实弹出了插入事件成功!!!,事件也确实出现在日历列表里,但到了设定时间,手机安安静静,没有任何提醒。代码看起来完全照着老教程写的——ContentResolver先查calanderURL拿calId,再往calanderEventURL插事件,最后往Reminders.CONTENT_URI插MINUTES、EVENT_ID、METHOD_ALERT三件套。链路一步没少,提醒就是不触发。
这类问题的麻烦之处在于:它不报错、不崩溃,insert返回的Uri也正常,getLastPathSegment()能解析出id,所以从日志上看一切"成功"。真正出问题的地方藏在三个容易被忽略的细节里——查到的calId到底是不是目标 Gmail 账户、hasAlarm和Reminders是不是只写了其中一处、时区字符串是不是合法的 IANA 值。这三处任意一个不对,提醒都会静默失效。
本篇把这条 query/insert 链路交给走 TaoToken 的 Codex 逐行对照排查。TaoToken 在这里只负责给 Codex 提供 Key 和调用通道,光标和日历 API 仍然由你自己的 Android 工程执行。先打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册并创建 Key,把 Base URL 填成 https://taotoken.net/api(注意不带/v1、不加 UTM),接进 Codex 之后,把上面那段代码和真机日志一起贴给它,让 Codex 帮你核对calId取的是哪条_id、Reminders.METHOD_ALERT是否真被插入、dtstart/dtend的 millis 是否落到预期日期。跑通后回去看插入事件是否返回id、Toast是否输出插入事件成功!!!,确认提醒真的落到日历上。
二、TaoToken 前置:给 Codex 一条稳定的调用通道
在开始排查之前,先把 Codex 的接入配好。这一步的目的不是"换个模型",而是让 Codex 有一个稳定、可复现的通道来读你的代码片段和日志——排查日历提醒这种问题,往往需要反复贴代码、反复追问,通道不稳定会打断思路。
操作顺序很简单:
- 打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册账号;
- 进入控制台创建 API Key,地址是 https://taotoken.net/api-keys ;
- 记下你的 Key,后面配置里用
YOUR_API_KEY占位,实际替换成你自己的; - Base URL 统一填
https://taotoken.net/api,不要加/v1,也不要带任何 UTM 参数。
如果你用的是 Codex CLI,可以直接用命令行接入:
npm i -g @taotoken/taotoken taotoken cc -k YOUR_API_KEY -u https://taotoken.net/api -m MODEL_ID这里的MODEL_ID填你在控制台看到的可用模型标识。接好之后,Codex 就能读取你贴进去的 Java/Kotlin 代码和logcat输出,逐行帮你比对字段。
需要强调的是:TaoToken 只提供 Key 和通道,它不会替你去调 Android 的ContentResolver,也不会替你操作日历。真正的插入动作仍然发生在你的 App 进程里,Codex 的角色是"对着代码和日志做静态核对与推理"。
三、可复制配置:把 query/insert 链路整理成可核对的形式
排查之前,先把原始代码整理成 Codex 容易读的形式。下面这段是典型的"查 calId → 插事件 → 插 Reminders"三段式,我把它补上了关键注释,方便逐行对照:
// 第一段:查 calId String calId = ""; Cursor userCursor = getContentResolver().query( Uri.parse(calanderURL), null, null, null, null); if (userCursor.getCount() > 0) { userCursor.moveToFirst(); calId = userCursor.getString(userCursor.getColumnIndex("_id")); } // 第二段:插事件 Calendar beginTime = Calendar.getInstance(); beginTime.set(2013, 10, 26, 16, 20); long startMillis = beginTime.getTimeInMillis(); Calendar endTime = Calendar.getInstance(); endTime.set(2013, 10, 26, 17, 20); long endMillis = endTime.getTimeInMillis(); ContentValues event = new ContentValues(); event.put("title", "测试完成2"); event.put("description", "真的好开心可以添加提醒了.lol~"); event.put("calendar_id", calId); event.put("dtstart", startMillis); event.put("dtend", endMillis); event.put("hasAlarm", 1); event.put(Events.EVENT_TIMEZONE, "China/Beijing"); Uri newEvent = getContentResolver().insert( Uri.parse(calanderEventURL), event); long id = Long.parseLong(newEvent.getLastPathSegment()); Toast.makeText(this, id + event.get("title").toString(), Toast.LENGTH_SHORT).show(); // 第三段:插 Reminders ContentValues values = new ContentValues(); values.put(Reminders.MINUTES, 10); values.put(Reminders.EVENT_ID, id); values.put(Reminders.METHOD, Reminders.METHOD_ALERT); getContentResolver().insert(Reminders.CONTENT_URI, values); Toast.makeText(this, "插入事件成功!!!", Toast.LENGTH_SHORT).show();把这段代码连同真机logcat一起贴给 Codex 时,建议在提问里明确三个核对点:
calId取到的是CalendarContract.Calendars里的哪一条_id?它的ACCOUNT_NAME是不是目标 Gmail 账户?Reminders.METHOD_ALERT这一行是否真的执行到了?有没有被异常吞掉?dtstart/dtend的 millis 换算成日期后,是不是你预期的 2013-11-26?
Codex 会基于你贴的代码和日志做推理,指出哪一段可能偏离预期。注意它不会"运行"你的代码,所以日志越完整,结论越可靠。
四、验证请求与成功结果:三个必须亲眼确认的信号
排查日历提醒,不能只看Toast。Toast只证明代码执行到了那一行,不证明数据真的写对了。下面三个信号必须逐一确认。
信号一:插入事件返回的id是否有效。newEvent.getLastPathSegment()解析出的id应该是非空、可转为long的字符串。如果newEvent为null,说明插入被拒绝,后面的Reminders插入会带着一个无效EVENT_ID,提醒自然不触发。可以在Toast里把id打出来,或者直接Log.d输出。
信号二:Reminders插入是否真的落库。很多人只插了事件、忘了插Reminders,或者反过来只写了hasAlarm=1却没插Reminders。这两处必须同时存在:hasAlarm告诉日历"这个事件有提醒",Reminders表则记录"提前多少分钟、用什么方式提醒"。只写一处,提醒都不会响。验证方法是插入后立刻反查:
Cursor rc = getContentResolver().query( Reminders.CONTENT_URI, null, Reminders.EVENT_ID + "=?", new String[]{String.valueOf(id)}, null); Log.d("CAL", "reminders count = " + rc.getCount());如果count为 0,说明Reminders没插进去,问题就定位到了第三段。
信号三:时区字符串是否合法。原始代码里写的是"China/Beijing",这不是合法的 IANA 时区 ID。正确的写法应该是"Asia/Shanghai"。非法的时区值可能导致事件时间被错误解释,提醒触发时间随之偏移甚至不触发。把Events.EVENT_TIMEZONE改成"Asia/Shanghai"后再测一次。
当这三个信号都确认无误,回到日历 App 里看:事件出现在目标 Gmail 账户下,时间正确,并且事件详情里能看到"提前 10 分钟提醒"。到这一步,提醒才算真正落到日历上。
五、本篇常见错排查:对照清单逐条过
把上面几个断点整理成一张排查清单,遇到"Toast 弹了但没提醒"时按顺序过一遍:
- calId 取错账户:
query(calanderURL)返回的第一条不一定是目标 Gmail 账户。应该用Calendars.ACCOUNT_NAME过滤,明确指定hoohbood@gmail.com这类账户,而不是无脑moveToFirst()。 - hasAlarm 与 Reminders 只写一处:两者必须同时存在。只写
hasAlarm=1不插Reminders,或只插Reminders不写hasAlarm,提醒都不触发。 - 时区写成非 IANA 值:
"China/Beijing"是错的,改成"Asia/Shanghai"。同理,"GMT+8"这类写法也不推荐。 - dtstart/dtend 的 millis 落错日期:
Calendar.set(2013, 10, 26, ...)里的月份是从 0 开始的,10代表 11 月。如果你以为是 10 月,日期就整体偏了一个月,提醒自然对不上。 - Reminders.METHOD 用错:
METHOD_ALERT是弹窗提醒,METHOD_DEFAULT走系统默认。如果设备把默认提醒关掉了,用METHOD_DEFAULT可能不响,排查时优先用METHOD_ALERT。 - 权限缺失:写日历需要
WRITE_CALENDAR和READ_CALENDAR,Android 6.0 以上还要运行时申请。权限没给,insert可能返回null而不抛异常。
把这份清单和你的代码一起贴给 Codex,让它逐条对照,通常能快速定位到具体是哪一处断了。
六、语义一致收尾:把通道和排查分开看
回到最初的问题:Toast弹了、事件也插了,提醒却不响。根因几乎总是落在calId、hasAlarm/Reminders、时区这三处之一。Codex 在这里的价值是帮你逐行核对代码和日志,把"看起来成功"和"数据真的写对"区分开;而 TaoToken 负责的是给 Codex 提供稳定的 Key 和调用通道,不介入你的 Android 工程执行。
如果你在排查过程中需要反复贴代码、追问细节,建议先把 Key 和接入配好:创建 Key 走 https://taotoken.net/api-keys ,接入文档看 https://taotoken.net/doc ,需要直接和模型对话核对代码片段可以用 https://taotoken.net/model-chat 。长期做 Android 端编码和 Agent 类任务的话,Coding Plan 会更合适:https://taotoken.net/coding-plan 。
通道归通道,日历 API 归日历 API。把这两件事分开,排查思路会清晰很多。