Files
FamilyBook_harmonyos/entry/src/main/ets/entryformability/FbFormAbility.ets
T

194 lines
10 KiB
Plaintext
Raw Normal View History

/**
* 家书 FamilyBook · 服务卡片提供方(鸿蒙端 FormExtensionAbility)
*
* 用户 2026-10-20:「跟日程有关的桌面服务卡片,你就完全按照 synccalendar 来做就行。」
* ⇒ 生命周期与翻页照 `SyncCalendar/entry/src/main/ets/entryformability/EntryFormAbility.ets`:
* · `onAddForm` —— 添加到桌面时先塞"空壳",再异步推真实数据,并**登记**这张卡;
* · `onUpdateForm` —— 系统定时刷新(`updateDuration: 1` ⇒ 30 分钟);
* · `onFormEvent` —— 卡片上的 ‹ / › / 今 三个按钮(`action: 'message'`);
* · `onRemoveForm` —— 移出桌面(注销 + 清掉它的翻页偏移)。
*
* 🔴 四个必须记住的约束:
* ① **生命周期回调返回后进程只活约 10 秒**(官方文档)⇒ 回调里不能做长任务。
* 这里的做法与 SyncCalendar 一致:立刻返回/用本地缓存,真实网络请求放在
* `.then()` 里异步推 —— 就算被回收,也只是"这次没刷成",30 分钟后还会重试。
* ② `onAddForm` **必须同步返回** `FormBindingData`(不能是 Promise),否则桌面
* 直接显示"卡片加载失败"。
* ③ 数据构建失败**绝不能抛**:抛出去的表现是桌面一张白卡,用户完全不知道发生了什么。
* ⇒ 失败也返回"带一句人话"的壳(`FbCardData.hint`)。
* ④ **翻页 = 每张卡一份偏移量**(`FB_CARD_FORMS`),推数据时按各自偏移构建 ——
* 系统定时刷新**不能**把用户翻走的日期拽回今天。
*/
import { FormExtensionAbility, formBindingData, formProvider, formInfo } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { fbCardDataByKind } from '../data/FbCardMediaData';
import { FB_CARD_FORMS } from '../data/FbCardForms';
import { FB_BG_SYNC } from '../common/FbBackgroundSync';
const DOMAIN: number = 0x0000;
const TAG: string = 'FbForm';
/** 卡片名 → 数据类型(v90 起不止日程卡)。
* ⚠️ 名字来自 `form_config.json` 的 `name` 字段(Want 里的 `form_name`),
* 两边改名必须同步 —— 改一边的表现是"新卡永远拿日程数据",编译不报错。 */
function kindOfFormName(name: string): string {
if (name.indexOf('FbMovieCard') === 0) {
return 'movies';
}
if (name.indexOf('FbTaskCard') === 0) {
return 'tasks';
}
return 'calendar';
}
export default class FbFormAbility extends FormExtensionAbility {
/**
* 重新算一遍并推给指定卡片。
*
* ⚠️ v90 起按类型分流:日程卡吃 `dayOffset`(翻页);
* 观影/任务卡没有"看哪一天"的概念,`dayOffset` 固定传 0。
*/
private push(formId: string, kind: string, dayOffset: number): void {
fbCardDataByKind(this.context, kind, dayOffset).then((data: Object): void => {
const binding: formBindingData.FormBindingData = formBindingData.createFormBindingData(data);
formProvider.updateForm(formId, binding).then((): void => {
hilog.info(DOMAIN, TAG, '卡片 %{public}s(%{public}s) 已更新(偏移 %{public}d)',
formId, kind, dayOffset);
}).catch((err: BusinessError): void => {
// 卡片可能已经被用户删了 —— 打一条日志就够,不要重试
hilog.warn(DOMAIN, TAG, '卡片 %{public}s 更新失败:%{public}s', formId, err.message);
});
}).catch((err: BusinessError): void => {
hilog.error(DOMAIN, TAG, '卡片数据构建失败:%{public}s', err.message);
});
}
/** 从 Want 里取卡片 id(`ohos.extra.param.key.form_identity`) */
private formIdOf(want: Want): string {
if (want.parameters === undefined) {
return '';
}
const raw: Object | undefined = want.parameters['ohos.extra.param.key.form_identity'];
return typeof raw === 'string' ? raw : '';
}
/** 从 Want 里取卡片名(`ohos.extra.param.key.form_name`,即 form_config 的 name) */
private formNameOf(want: Want): string {
if (want.parameters === undefined) {
return '';
}
const raw: Object | undefined = want.parameters[formInfo.FormParam.NAME_KEY];
return typeof raw === 'string' ? raw : '';
}
onAddForm(want: Want): formBindingData.FormBindingData {
const formId: string = this.formIdOf(want);
const kind: string = kindOfFormName(this.formNameOf(want));
hilog.info(DOMAIN, TAG, '添加卡片:%{public}s(%{public}s)', formId, kind);
if (formId !== '') {
// 异步:登记 + 补真实数据(不 await:见文件头约束 ①)
FB_CARD_FORMS.register(this.context, formId, kind).then((): void => {
this.push(formId, kind, 0);
}).catch((err: BusinessError): void => {
hilog.warn(DOMAIN, TAG, '卡片登记失败:%{public}s', err.message);
});
}
// 先给一份"正在取"的壳 —— 卡片上会显示空态,而不是白屏
const shell: Record<string, string> = {};
shell['state'] = 'loading';
shell['hint'] = '正在取数据…';
return formBindingData.createFormBindingData(shell);
}
onUpdateForm(formId: string): void {
hilog.info(DOMAIN, TAG, '系统定时刷新:%{public}s', formId);
/* 🔴🔴 v83(用户 2026-10-23「要有定期同步的功能,家书app在后台,也能自动同步数据」):
**借卡片定时刷新这个时机跑一轮后台同步。**
为什么只有这里能做到"约 30 分钟一次":
`workScheduler` 的周期被系统按"应用活跃分组"压制(活跃≥2h / 常用≥24h),
请求更短不报错但**不生效**;而卡片配了 `updateDuration: 1`(单位 30 分钟)后,
系统会每 30 分钟回调一次本方法,**不受活跃分组压制**(每天上限 50 次)。
⇒ 这是那条"打开 app 数据就是最新的"真正落地的通道。
⚠️ **必须先推卡片、再同步**:
推送只依赖本地缓存/一次轻量请求,几秒内就能完成;
同步要跑 12 个模块,很可能在"进程只活 10 秒"的窗口里被砍。
顺序反了就会变成"同步把时间用光、卡片这次没刷新"——
用户看到的是"卡片一直转圈",比慢一步更糟。
⚠️ **不 await**:`onUpdateForm` 返回后进程只剩约 10 秒,
await 只会把两者一起拖死。同步自己内部有节流与预算(见 FbBackgroundSync)。
⚠️ 节流:系统刷新可能漂移或重入,`runThrottledByCard` 内部用时间戳挡了一道,
并且**先写时间戳再跑**,避免卡片进程与主进程并发拉同一批数据。 */
// ⚠️ 必须按**这张卡自己的偏移**重建,否则定时刷新会把用户翻走的日期拽回今天
// 🔴 v90:偏移量只对日程卡有意义;观影/任务卡按类型直接推。
FB_CARD_FORMS.kindOf(this.context, formId).then((kind: string): void => {
if (kind !== 'calendar') {
this.push(formId, kind, 0);
return;
}
FB_CARD_FORMS.getOffset(this.context, formId).then((off: number): void => {
this.push(formId, kind, off);
}).catch((err: BusinessError): void => {
hilog.warn(DOMAIN, TAG, '读卡片偏移失败:%{public}s', err.message);
this.push(formId, kind, 0);
});
}).catch((err: BusinessError): void => {
hilog.warn(DOMAIN, TAG, '读卡片类型失败(按日程处理):%{public}s', err.message);
this.push(formId, 'calendar', 0);
});
/* ⚠️ 这一句必须放在**推送之后**(原因见上面的 ①:卡片刷新只有几秒的窗口,
不能被 12 个模块的同步挤掉)。两条都是"发起即返回",不会互相阻塞。 */
FB_BG_SYNC.runThrottledByCard(this.context).catch((err: BusinessError): void => {
hilog.warn(DOMAIN, TAG, '卡片驱动的后台同步失败(不影响卡片本身):%{public}s', err.message);
});
}
onFormEvent(formId: string, message: string): void {
/* 卡片上的按钮都走 `action: 'message'`,`message` 是 JSON 字符串。
⚠️ 不解析 JSON:不同版本系统给的形态可能是 `{params:{...}}` 或直接 `{...}`,
子串匹配更稳(与 SyncCalendar 同一个取舍)。
⚠️ v90:只有日程卡有 ‹ / › / 今 三个按钮;观影/任务卡不发 message 事件,
收到也直接忽略(不把 message 撞进偏移量里)。 */
hilog.info(DOMAIN, TAG, '卡片事件:%{public}s %{public}s', formId, message);
FB_CARD_FORMS.kindOf(this.context, formId).then((kind: string): void => {
if (kind !== 'calendar') {
return;
}
FB_CARD_FORMS.getOffset(this.context, formId).then((cur: number): void => {
let next: number = cur;
if (message.indexOf('prev') >= 0) {
next = cur - 1;
} else if (message.indexOf('next') >= 0) {
next = cur + 1;
} else if (message.indexOf('today') >= 0) {
next = 0;
}
FB_CARD_FORMS.setOffset(this.context, formId, next).then((off: number): void => {
this.push(formId, kind, off);
}).catch((err: BusinessError): void => {
hilog.warn(DOMAIN, TAG, '卡片翻页失败:%{public}s', err.message);
this.push(formId, kind, cur);
});
}).catch((err: BusinessError): void => {
hilog.warn(DOMAIN, TAG, '读卡片偏移失败(按今天处理):%{public}s', err.message);
this.push(formId, kind, 0);
});
}).catch((err: BusinessError): void => {
hilog.warn(DOMAIN, TAG, '读卡片类型失败:%{public}s', err.message);
});
}
onRemoveForm(formId: string): void {
hilog.info(DOMAIN, TAG, '移除卡片:%{public}s', formId);
FB_CARD_FORMS.unregister(this.context, formId).then((): Promise<void> => {
return FB_CARD_FORMS.clearOffset(this.context, formId);
}).catch((err: BusinessError): void => {
hilog.warn(DOMAIN, TAG, '注销卡片失败:%{public}s', err.message);
});
}
}