后台同步重构:移除不合规长时任务,改为分层调度
背景:用户反馈"后台常驻其实没实现,过一段时间常驻通知就没了"。调研确认这是 系统强制管控、不是 bug:DATA_TRANSFER 长时任务有一致性校验,**超 10 分钟未更新 传输进度即被强制取消**。日历同步是"间歇跑、平时无进度",不符合该语义 → 必被取消, 且频繁触发校验失败可能被列入管控名单。另:workScheduler 请求 20 分钟低于系统 "应用活跃分组"下限(活跃≥2h),被静默压制 → 名义 20 分钟实际从未生效。 改为分层调度(目标:用户要看时数据一定最新,而非追求进程常驻): 1. **移除 dataTransfer 长时任务**:BackgroundSyncService 不再申请; module.json5 去掉 backgroundModes 与 ohos.permission.KEEP_BACKGROUND_RUNNING 2. **延迟任务周期 20 分钟 → 2 小时**(对齐系统下限,避免被压制);保留 isRepeat/isPersisted 3. **新增 BackgroundSyncRunner**:后台同步唯一执行体(抽取自 SyncWorkAbility), 供延迟任务/网络恢复/回到前台三处共用,避免逻辑漂移 4. **新增 NetworkSyncWatcher**:net.connection 监听,**从无网恢复到有网时立即补同步** (带 20s 去抖;仅在 netLost→netAvailable 时触发,单纯切网不重复同步) 5. **回到前台补同步**:UIAbility.onForeground 立即同步 + 取消短时任务归还配额 6. **退后台短时任务**:onBackground 用 requestSuspendDelay 兜住退后台那次同步; 回到前台 cancelSuspendDelay(配额仅 10 分钟/日,必须归还) 7. **设置页开关语义调整**:"后台持续同步(常驻通知)" → "后台自动同步", 文案如实说明"系统最短约 2 小时一次 + 回前台/网络恢复立即补同步"; 开关现在真正控制延迟任务的注册/取消 8. **官网文档同步**:sf/uh 改写后台同步说明;upa **移除 KEEP_BACKGROUND_RUNNING 权限行** (权限表必须与实际声明一致);已重建 404 并部署线上 注:applyEfficiencyResources 在当前 SDK 不可用,未采用。
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
// entry/src/main/ets/syncwork/SyncWorkAbility.ets
|
||||
// 延迟任务兜底同步:App 进程被杀/设备重启后,系统在满足条件(有网络)时拉起本扩展执行同步
|
||||
// 注意:必须幂等——同步本身基于 ETag 全量比对,天然幂等;执行完必须 stopWork,否则系统按超时处理
|
||||
// 延迟任务兜底同步:App 进程被杀/设备重启后,系统在满足条件(有网络)时拉起本扩展执行同步。
|
||||
//
|
||||
// ⚠️ 约束(勿改):
|
||||
// - 单次回调**上限 2 分钟**,超时进程被系统终止 → 逻辑必须轻量(本类只调 Runner,不做事后处理)。
|
||||
// - 周期由 BackgroundSyncService 设为 2 小时(系统活跃分组下限),此处不再重复设置。
|
||||
// - 必须幂等:同步基于 etag 全量比对 + dirty 标记,天然幂等。
|
||||
// - 必须主动 stopWork,否则系统按超时处理。
|
||||
import { WorkSchedulerExtensionAbility, workScheduler } from '@kit.BackgroundTasksKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { DavAccount, AccountStore, TYPE_CALDAV } from '../common/AccountStore';
|
||||
import { SyncEngine } from '../common/SyncEngine';
|
||||
import { CardDataService } from '../common/CardDataService';
|
||||
import { ReminderService } from '../common/ReminderService';
|
||||
import { SystemCalendarImport } from '../common/SystemCalendarImport';
|
||||
import { SystemCalendarMirror } from '../common/SystemCalendarMirror';
|
||||
import { MirrorInbound } from '../common/MirrorInbound';
|
||||
import { AppSettings } from '../common/AppSettings';
|
||||
import { BackgroundSyncRunner } from '../common/BackgroundSyncRunner';
|
||||
import { LogUtil } from '../common/LogUtil';
|
||||
|
||||
export default class SyncWorkAbility extends WorkSchedulerExtensionAbility {
|
||||
@@ -26,65 +23,12 @@ export default class SyncWorkAbility extends WorkSchedulerExtensionAbility {
|
||||
|
||||
private async doSync(workInfo: workScheduler.WorkInfo): Promise<void> {
|
||||
try {
|
||||
const context = this.context;
|
||||
const accounts: DavAccount[] = await AccountStore.loadAll(context);
|
||||
// 备份模式:先导入系统本地日历(幂等),再随同步推送
|
||||
try {
|
||||
await SystemCalendarImport.importIfNeeded(context);
|
||||
} catch (err) {
|
||||
// 导入失败不影响正常同步
|
||||
}
|
||||
// 入站对账:把用户在系统日历里的改动读回来置 dirty(必须在同步之前)
|
||||
try {
|
||||
await MirrorInbound.reconcile(context);
|
||||
} catch (err) {
|
||||
// 对账失败不影响正常同步
|
||||
}
|
||||
let ok: number = 0;
|
||||
let attempted: number = 0;
|
||||
for (const acc of accounts) {
|
||||
if (acc.type !== TYPE_CALDAV) {
|
||||
continue;
|
||||
}
|
||||
attempted++;
|
||||
try {
|
||||
// 超时保护由 syncAccount 内部处理(全量重拉 10 分钟 / 常规 5 分钟),不再套外层超时
|
||||
await SyncEngine.syncAccount(context, acc);
|
||||
ok++;
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
LogUtil.write(`延迟任务同步 id=${acc.id} 失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
// 全部账号同步成功才结束"一次性全量重拉"状态
|
||||
if (attempted > 0 && ok === attempted) {
|
||||
await AppSettings.markFullRefetchDone(context);
|
||||
}
|
||||
await SyncEngine.settleLocalEvents(context);
|
||||
await SyncEngine.pruneOrphanRows(context, accounts);
|
||||
// ⚠️ 必须回写账号:syncAccount 会就地更新 acc(服务器日历本颜色、写权限等)。
|
||||
// 缺了这一步,后台同步刷到的颜色只留在内存里、进程退出即丢失,
|
||||
// 界面下次冷启动仍用旧颜色(表现为"同步之后颜色还是不对")。
|
||||
await AccountStore.saveAll(context, accounts);
|
||||
// 出站镜像:把服务器最新状态写进系统日历(自动化,无需手点)
|
||||
try {
|
||||
if (await AppSettings.getMirrorEnabled(context) && await SystemCalendarMirror.hasPermission()) {
|
||||
const mr = await SystemCalendarMirror.syncNow(context);
|
||||
LogUtil.write(`延迟任务自动镜像:本=${mr.books} 新增=${mr.added} 更新=${mr.updated} 删除=${mr.deleted}`);
|
||||
}
|
||||
} catch (err) {
|
||||
// 镜像失败不影响同步
|
||||
}
|
||||
// 同步后刷新卡片 + 重建提醒,保证后台同步的成果直接可见
|
||||
await CardDataService.pushToAllForms(context);
|
||||
await ReminderService.refreshReminders(context);
|
||||
LogUtil.write(`延迟任务同步完成:${ok} 个账号`);
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
LogUtil.write(`延迟任务同步失败: ${e.message}`);
|
||||
// 完整的后台同步流程(导入/对账/逐账号同步/镜像/卡片/提醒)统一由 Runner 实现,
|
||||
// 与"网络恢复补同步"、"回到前台补同步"共用同一套逻辑,避免三处实现漂移。
|
||||
await BackgroundSyncRunner.runOnce(this.context, 'deferred');
|
||||
} finally {
|
||||
// 必须主动结束,否则系统按 120 秒超时处理
|
||||
workScheduler.stopWork(workInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user