背景:为什么要把里程碑写进系统日历
“轻规划”(AeroPlan)的目标是把大目标拆解为可执行里程碑。原文指出,计划流产的核心痛点之一是“做好了规划,但过几天就忘了主动去看”。如果待办只锁在 App 内部,用户没有打开习惯,通知又可能因进程被查杀、保活受限而触达失败,规划系统就容易瘫痪。HarmonyOS NEXT 的 Calendar Kit 提供了直接操纵系统级日程的底座。把拆解出的行动和习惯日程写入系统日历后,即使应用进程被终止,系统日历通知仍由系统托管按时提醒;桌面日历元服务小组件可渲染当天关键任务;小艺等系统智能语音助理也能读取日程,提供出行与规划提醒。
一、同步管线:业务层、桥接器和系统日历服务
原文把链路拆成四层:应用业务层负责用 AI 拆解大目标,计算开始时间与持续时间;数据桥接器 CalendarHelper 与 CalendarSyncMutex 负责 Context 兼容和并发控制;Calendar Kit API 通过底层 IPC 写入系统内置 SQLite;系统级呈现层由设备系统直接渲染,不受应用生命周期约束。对开发者而言,这意味着写入操作要异步执行,避免阻塞主线程,同时要处理好前后台 Context 与并发冲突。
二、权限申请与默认日历账户获取
日历读写属于敏感高危操作。首先在 entry/src/main/module.json5 的 requestPermissions 中静态声明 ohos.permission.READ_CALENDAR 和 ohos.permission.WRITE_CALENDAR。每个权限的 reason 字段必须在 resources/base/element/string.json 中配置清晰、明确的中文理由,否则华为应用市场上架审核可能因“权限申请理由描述不清”被拒。usedScene 中可配置 abilities 为 EntryAbility、when 为 inuse。
运行时再通过 abilityAccessCtrl 动态申请,授权结果中 code 为 0 表示成功;随后调用 calendarManager.getCalendarManager(context) 获取管理器,并通过 getDefaultCalendar() 绑定系统默认日历账户。核心初始化逻辑如下:
- import { calendarManager } from '@kit.CalendarKit';
- import { abilityAccessCtrl, common } from '@kit.AbilityKit';
- import { BusinessError } from '@kit.BasicServicesKit';
- export class CalendarHelper {
- private calendarMgr: calendarManager.CalendarManager | null = null;
- private defaultCalendar: calendarManager.Calendar | null = null;
- public async init(context: common.UIAbilityContext): Promise<boolean> {
- const atManager = abilityAccessCtrl.createAtManager();
- try {
- const grantResult = await atManager.requestPermissionsFromUser(context, [
- 'ohos.permission.READ_CALENDAR',
- 'ohos.permission.WRITE_CALENDAR'
- ]);
- const isGranted = grantResult.authResults.every(res => res === 0);
- if (!isGranted) {
- console.warn('CalendarHelper', 'Calendar permissions denied by user.');
- return false;
- }
- this.calendarMgr = calendarManager.getCalendarManager(context);
- this.defaultCalendar = await this.calendarMgr.getDefaultCalendar();
- return true;
- } catch (err) {
- console.error('CalendarHelper', `Init calendar manager failed: ${(err as BusinessError).message}`);
- return false;
- }
- }
- }
复制代码
三、写入带强提醒的里程碑 Event
写入时最关键的是时间窗和提醒规则。原文使用 startTime、endTime 描述日程区间,把 type 设为 calendarManager.EventType.IMPORTANT,让系统用醒目样式并赋予更高提醒优先级;remindTimes: [15] 表示事件开始前 15 分钟发送系统级通知。标题可加应用前缀,如 [轻规划]。示例:
- public async addMilestoneSchedule(
- title: string,
- description: string,
- startTimeMs: number,
- durationMinutes: number
- ): Promise<void> {
- if (!this.defaultCalendar) {
- throw new Error('Calendar Helper not initialized or default calendar missing');
- }
- const endTimeMs = startTimeMs + (durationMinutes * 60 * 1000);
- const event: calendarManager.Event = {
- title: `[轻规划] ${title}`,
- description: description,
- type: calendarManager.EventType.IMPORTANT,
- startTime: startTimeMs,
- endTime: endTimeMs,
- remindTimes: [15]
- };
- try {
- await this.defaultCalendar.addEvent(event);
- } catch (error) {
- console.error('CalendarHelper', `Insert event failed: ${(error as BusinessError).message}`);
- throw error;
- }
- }
复制代码
四、后台静默 AutoSync 的两个坑
轻规划提供 AutoSync 自动后台同步:当后台 AI 服务或小艺建议识别到日程冲突并重排时间线时,应用需静默更新日历。原文在这里踩过两个典型问题。
坑 1:非 UI 上下文调用 getCalendarManager 报 401
在 WorkScheduler 长时任务或 ExtensionAbility 中直接调用 calendarManager.getCalendarManager(context),如果传入前台 UIAbilityContext 或 null 句柄,系统会抛出 BusinessError 401: Invalid context。原因是后台任务生命周期与宿主 Ability 独立,没有前台界面。解决方式是使用 context.getApplicationContext() 获取全局上下文,再获取日历管理器:
- const appContext = context.getApplicationContext();
- this.calendarMgr = calendarManager.getCalendarManager(appContext);
复制代码
坑 2:前后台并发写导致 relationalStore 锁死
系统日历底层是 SQLite 关系型数据库,并通过 IPC 提供服务。后台定时静默同步可能在写入多条日程,前台用户也可能同时手动点击同步,两端并发写同一张表会形成写竞争,触发底层 relationalStore 锁死,抛出数据库繁忙错误,甚至让同步模块崩溃。原文的做法是在应用内加一层内存级悲观互斥锁 CalendarSyncMutex:任何日历同步读写都走 executeSync,若已有同步事务在执行,就直接丢弃冗余请求;无论成功失败都在 finally 中释放锁,避免死锁。
- export class CalendarSyncMutex {
- private static isSyncing = false;
- public static async executeSync(action: () => Promise<void>): Promise<void> {
- if (this.isSyncing) {
- console.warn('CalendarSyncMutex', 'An active calendar sync transaction is running. Request discarded.');
- return;
- }
- this.isSyncing = true;
- try {
- await action();
- } catch (error) {
- console.error('CalendarSyncMutex', `Error occurred during sync transaction: ${(error as Error).message}`);
- } finally {
- this.isSyncing = false;
- }
- }
- }
复制代码
业务层调用时,用 CalendarSyncMutex.executeSync 包住 addMilestoneSchedule 等写入或更新操作,就可以把前后台同步请求串行化。
五、小结
打通 Calendar Kit 后,应用不必完全依赖自身进程存活来提醒用户。原文总结的关键点是:静态与动态权限双保险、后台使用全局 ApplicationContext 初始化、用内存级悲观互斥锁治理并发。这样既能提高里程碑触达率,也能规避后台静默同步中的 Context 失效和数据库并发死锁。原文还预告下一篇将进入 Multimodal Awareness Kit,讨论智感握姿感知、悬浮操作面板与输入框主手热区自适应切换。 |