添加了后台同步功能,应用置于后台,仍然可以进行同步。

This commit is contained in:
2026-09-13 16:33:53 +08:00
parent 0a204b6754
commit e27c8c36d7
8 changed files with 251 additions and 1 deletions
+24
View File
@@ -8,6 +8,30 @@ export class AppSettings {
private static readonly STORE: string = 'sync_settings';
private static readonly KEY_SHOW_SYSTEM: string = 'show_system_calendar';
private static readonly KEY_SYNC_INTERVAL: string = 'sync_interval_minutes';
private static readonly KEY_BACKGROUND_SYNC: string = 'background_sync';
/** 是否开启后台持续同步(长时任务,默认关) */
static async getBackgroundSync(context: common.Context): Promise<boolean> {
try {
const store: preferences.Preferences =
await preferences.getPreferences(context, AppSettings.STORE);
return await store.get(AppSettings.KEY_BACKGROUND_SYNC, false) as boolean;
} catch (err) {
return false;
}
}
static async setBackgroundSync(context: common.Context, value: boolean): Promise<void> {
try {
const store: preferences.Preferences =
await preferences.getPreferences(context, AppSettings.STORE);
await store.put(AppSettings.KEY_BACKGROUND_SYNC, value);
await store.flush();
} catch (err) {
const e = err as BusinessError;
console.error(`保存后台同步设置失败: ${e.message}`);
}
}
/** 是否混合显示系统日历日程(默认开) */
static async getShowSystemCalendar(context: common.Context): Promise<boolean> {
@@ -0,0 +1,104 @@
// entry/src/main/ets/common/BackgroundSyncService.ets
// 后台同步管理:
// 1) 长时任务(DATA_TRANSFER):进程不被挂起,前台定时器在后台继续跑(需用户在设置中开启)
// 2) 延迟任务(workScheduler):App 被杀/重启后由系统按条件拉起兜底同步(最小周期 20 分钟)
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { wantAgent, common } from '@kit.AbilityKit';
import { workScheduler } from '@kit.BackgroundTasksKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { AppSettings } from './AppSettings';
import { LogUtil } from './LogUtil';
const WORK_ID: number = 1001;
export class BackgroundSyncService {
/** 申请长时任务(DATA_TRANSFER):申请成功后进程在后台不被挂起 */
static async startContinuousTask(context: common.UIAbilityContext): Promise<void> {
try {
const info: wantAgent.WantAgentInfo = {
wants: [{
bundleName: context.abilityInfo.bundleName,
abilityName: 'EntryAbility'
}],
operationType: wantAgent.OperationType.START_ABILITY,
requestCode: 0
};
const agent = await wantAgent.getWantAgent(info);
await backgroundTaskManager.startBackgroundRunning(
context, backgroundTaskManager.BackgroundMode.DATA_TRANSFER, agent);
LogUtil.write('长时任务已申请:后台持续同步开启');
} catch (err) {
const e = err as BusinessError;
LogUtil.write(`长时任务申请失败: ${e.code} - ${e.message}`);
}
}
/** 停止长时任务 */
static async stopContinuousTask(context: common.UIAbilityContext): Promise<void> {
try {
await backgroundTaskManager.stopBackgroundRunning(context);
LogUtil.write('长时任务已停止');
} catch (err) {
const e = err as BusinessError;
LogUtil.write(`长时任务停止失败: ${e.code} - ${e.message}`);
}
}
/**
* 注册延迟任务兜底同步:无论长时任务开关与否都注册。
* 条件:有任意网络即执行;周期重复;重启后自动恢复。
* 注:repeatCycleTime 各版本下限不同(20 分钟 ~ 2 小时),先用 20 分钟,
* 被系统拒绝(9700003 条件无效)时降级为 2 小时再试。
*/
static scheduleDeferredSync(context: common.UIAbilityContext): void {
const build = (cycleMs: number): workScheduler.WorkInfo => {
const info: workScheduler.WorkInfo = {
workId: WORK_ID,
bundleName: context.abilityInfo.bundleName,
abilityName: 'SyncWorkAbility',
networkType: workScheduler.NetworkType.NETWORK_TYPE_ANY,
isRepeat: true,
isPersisted: true,
repeatCycleTime: cycleMs
};
return info;
};
try {
workScheduler.startWork(build(20 * 60 * 1000));
LogUtil.write('延迟任务已注册:20 分钟兜底同步');
} catch (err) {
const e = err as BusinessError;
if (e.code === 9700003) {
try {
workScheduler.startWork(build(2 * 60 * 60 * 1000));
LogUtil.write('延迟任务降级注册:2 小时兜底同步');
return;
} catch (err2) {
const e2 = err2 as BusinessError;
LogUtil.write(`延迟任务注册失败(2h): ${e2.code} - ${e2.message}`);
return;
}
}
LogUtil.write(`延迟任务注册失败: ${e.code} - ${e.message}`);
}
}
/** App 启动时统一入口:注册延迟任务 + 按设置恢复长时任务 */
static async initOnLaunch(context: common.UIAbilityContext): Promise<void> {
BackgroundSyncService.scheduleDeferredSync(context);
const enabled: boolean = await AppSettings.getBackgroundSync(context);
if (enabled) {
await BackgroundSyncService.startContinuousTask(context);
}
}
/** 设置页开关切换 */
static async setBackgroundSync(context: common.UIAbilityContext, enabled: boolean): Promise<void> {
await AppSettings.setBackgroundSync(context, enabled);
if (enabled) {
await BackgroundSyncService.startContinuousTask(context);
} else {
await BackgroundSyncService.stopContinuousTask(context);
}
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ export class SyncEngine {
* 同步一个 CalDAV 账号(先推该账号的本地修改,再拉远端变更),
* 返回远端事件总数(拉取侧)。带 120 秒超时保护。
*/
static async syncAccount(context: common.UIAbilityContext, acc: DavAccount): Promise<number> {
static async syncAccount(context: common.Context, acc: DavAccount): Promise<number> {
const t0: number = Date.now();
LogUtil.write(`========== 同步账号「${acc.name}」开始:服务器 ${acc.serverUrl},共 ${acc.calendarHrefs.length} 个日历本 ==========`);
try {
@@ -1,12 +1,15 @@
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { BackgroundSyncService } from '../common/BackgroundSyncService';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
// 后台同步:注册延迟任务兜底 + 按设置恢复长时任务
BackgroundSyncService.initOnLaunch(this.context);
}
onDestroy(): void {
+51
View File
@@ -3,6 +3,7 @@
import { router } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { AppSettings } from '../common/AppSettings';
import { BackgroundSyncService } from '../common/BackgroundSyncService';
import { LogUtil } from '../common/LogUtil';
const INTERVAL_OPTIONS: number[] = [1, 5, 15, 30, 60];
@@ -12,6 +13,7 @@ const INTERVAL_OPTIONS: number[] = [1, 5, 15, 30, 60];
struct SettingsPage {
@State showSystem: boolean = true;
@State intervalMinutes: number = 1;
@State backgroundSync: boolean = false;
private context?: common.Context;
aboutToAppear(): void {
@@ -27,6 +29,9 @@ struct SettingsPage {
AppSettings.getSyncIntervalMinutes(ctx).then((v: number): void => {
this.intervalMinutes = v;
});
AppSettings.getBackgroundSync(ctx).then((v: boolean): void => {
this.backgroundSync = v;
});
}
private async saveShowSystem(value: boolean): Promise<void> {
@@ -48,6 +53,24 @@ struct SettingsPage {
.showToast({ message: `自动同步间隔已设为 ${minutes} 分钟` });
}
private async saveBackgroundSync(value: boolean): Promise<void> {
if (this.context === undefined) {
return;
}
try {
await BackgroundSyncService.setBackgroundSync(
this.context as common.UIAbilityContext, value);
this.getUIContext().getPromptAction().showToast({
message: value
? '后台持续同步已开启,通知栏将显示常驻通知'
: '后台持续同步已关闭'
});
} catch (err) {
this.backgroundSync = !value; // 失败回滚 UI
this.getUIContext().getPromptAction().showToast({ message: '设置失败,请重试' });
}
}
private intervalLabel(minutes: number): string {
return minutes >= 60 ? `${minutes / 60} 小时` : `${minutes} 分钟`;
}
@@ -134,6 +157,34 @@ struct SettingsPage {
.borderRadius(12)
.backgroundColor($r('app.color.card_bg'))
.border({ width: 1, color: $r('app.color.shadow_color') })
// 后台持续同步(长时任务)
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('后台持续同步')
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.text_primary'))
Text('开启后申请后台长时任务,App 退到后台也按设定间隔继续同步(通知栏会显示常驻通知)')
.fontSize(12)
.fontColor($r('app.color.text_secondary'))
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.backgroundSync })
.selectedColor($r('app.color.brand'))
.onChange((isOn: boolean) => {
if (isOn !== this.backgroundSync) {
this.backgroundSync = isOn;
this.saveBackgroundSync(isOn);
}
})
}
.width('100%')
.padding(14)
.borderRadius(12)
.backgroundColor($r('app.color.card_bg'))
.border({ width: 1, color: $r('app.color.shadow_color') })
}
.width('100%')
.padding({ left: 20, right: 20, top: 8, bottom: 20 })
@@ -0,0 +1,54 @@
// entry/src/main/ets/syncwork/SyncWorkAbility.ets
// 延迟任务兜底同步:App 进程被杀/设备重启后,系统在满足条件(有网络)时拉起本扩展执行同步
// 注意:必须幂等——同步本身基于 ETag 全量比对,天然幂等;执行完必须 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 { LogUtil } from '../common/LogUtil';
export default class SyncWorkAbility extends WorkSchedulerExtensionAbility {
onWorkStart(workInfo: workScheduler.WorkInfo): void {
LogUtil.init(this.context);
LogUtil.write(`延迟任务触发:workId=${workInfo.workId}`);
this.doSync(workInfo);
}
onWorkStop(workInfo: workScheduler.WorkInfo): void {
LogUtil.write(`延迟任务结束:workId=${workInfo.workId}`);
}
private async doSync(workInfo: workScheduler.WorkInfo): Promise<void> {
try {
const context = this.context;
const accounts: DavAccount[] = await AccountStore.loadAll(context);
let ok: number = 0;
for (const acc of accounts) {
if (acc.type !== TYPE_CALDAV) {
continue;
}
try {
await SyncEngine.withTimeout(
SyncEngine.syncAccount(context, acc), 100000);
ok++;
} catch (err) {
const e = err as BusinessError;
LogUtil.write(`延迟任务同步「${acc.name}」失败: ${e.message}`);
}
}
await SyncEngine.settleLocalEvents(context);
// 同步后刷新卡片 + 重建提醒,保证后台同步的成果直接可见
await CardDataService.pushToAllForms(context);
await ReminderService.refreshReminders(context);
LogUtil.write(`延迟任务同步完成:${ok} 个账号`);
} catch (err) {
const e = err as BusinessError;
LogUtil.write(`延迟任务同步失败: ${e.message}`);
} finally {
// 必须主动结束,否则系统按 120 秒超时处理
workScheduler.stopWork(workInfo);
}
}
}
+10
View File
@@ -58,6 +58,9 @@
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"backgroundModes": [
"dataTransfer"
],
"skills": [
{
"entities": [
@@ -95,6 +98,13 @@
"resource": "$profile:form_config"
}
]
},
{
"name": "SyncWorkAbility",
"srcEntry": "./ets/syncwork/SyncWorkAbility.ets",
"description": "$string:sync_work_desc",
"type": "workScheduler",
"exported": false
}
]
},
@@ -47,6 +47,10 @@
{
"name": "card_6x4_name",
"value": "日程大全"
},
{
"name": "sync_work_desc",
"value": "日历后台兜底同步任务"
}
]
}