首次提交:SyncCalendar 项目
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
// entry/src/main/ets/common/EventDb.ets
|
||||
// 本地事件数据库(relationalStore):DAV 同步来的日程与本地新建日程统一存储
|
||||
import { relationalStore } from '@kit.ArkData';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
|
||||
/** 本地事件行 */
|
||||
export class LocalEvent {
|
||||
id: number = 0;
|
||||
uid: string = '';
|
||||
calKey: string = ''; // 'accId_idx' 或 'local'
|
||||
href: string = ''; // 所属日历本集合 URL(local 为空)
|
||||
remotePath: string = ''; // 集合内资源文件名,如 <uid>.ics(local 为空)
|
||||
title: string = '';
|
||||
description: string = '';
|
||||
location: string = '';
|
||||
startTime: number = 0; // 13 位毫秒;全天日程为当天 0 点
|
||||
endTime: number = 0; // 全天日程为排他结束日前一毫秒(沿用 iCal 约定减 1s 存储)
|
||||
isAllDay: boolean = false;
|
||||
etag: string = '';
|
||||
dirty: boolean = false; // 本地有修改,待推送
|
||||
deleted: boolean = false; // 本地已删除,待推送
|
||||
recurring: boolean = false; // 重复日程实例(暂不支持推送,避免破坏服务器序列)
|
||||
kind: string = 'event'; // 'event' 日程 | 'todo' 待办(VTODO,只读展示)
|
||||
completed: boolean = false; // 待办是否已完成(STATUS:COMPLETED)
|
||||
rrule: string = ''; // 原始 RRULE(空 = 非重复);显示时按规则展开多次发生
|
||||
exdate: string = ''; // 原始 EXDATE 排除日期,分号分隔
|
||||
reminder: number = 0; // 提醒提前分钟数(来自 VALARM,0 = 不提醒)
|
||||
}
|
||||
|
||||
/** 远端事件条目(REPORT 解析结果) */
|
||||
export class RemoteEvent {
|
||||
uid: string = '';
|
||||
etag: string = '';
|
||||
title: string = '';
|
||||
description: string = '';
|
||||
location: string = '';
|
||||
startTime: number = 0;
|
||||
endTime: number = 0;
|
||||
isAllDay: boolean = false;
|
||||
recurring: boolean = false; // 含 RRULE 或 RECURREIENCE-ID 的实例
|
||||
isTodo: boolean = false; // VTODO 待办
|
||||
completed: boolean = false; // VTODO STATUS:COMPLETED
|
||||
rrule: string = ''; // 原始 RRULE
|
||||
exdate: string = ''; // 原始 EXDATE(分号分隔)
|
||||
reminder: number = 0; // 提醒提前分钟数(VALARM)
|
||||
}
|
||||
|
||||
export class EventDb {
|
||||
private static db: relationalStore.RdbStore | null = null;
|
||||
|
||||
static async getDb(context: common.Context): Promise<relationalStore.RdbStore> {
|
||||
if (EventDb.db !== null) {
|
||||
return EventDb.db;
|
||||
}
|
||||
const config: relationalStore.StoreConfig = {
|
||||
name: 'sync_calendar.db',
|
||||
securityLevel: relationalStore.SecurityLevel.S1
|
||||
};
|
||||
const store = await relationalStore.getRdbStore(context, config);
|
||||
await store.executeSql(
|
||||
'CREATE TABLE IF NOT EXISTS events (' +
|
||||
'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
|
||||
'uid TEXT, cal_key TEXT, href TEXT, remote_path TEXT, ' +
|
||||
'title TEXT, description TEXT, location TEXT, ' +
|
||||
'start_time INTEGER, end_time INTEGER, is_all_day INTEGER, ' +
|
||||
'etag TEXT, dirty INTEGER, deleted INTEGER, recurring INTEGER)'
|
||||
);
|
||||
// 旧版本库补列(已存在会抛错,忽略)
|
||||
try {
|
||||
await store.executeSql('ALTER TABLE events ADD COLUMN recurring INTEGER DEFAULT 0');
|
||||
} catch (err) {
|
||||
// 列已存在
|
||||
}
|
||||
try {
|
||||
await store.executeSql("ALTER TABLE events ADD COLUMN kind TEXT DEFAULT 'event'");
|
||||
} catch (err) {
|
||||
// 列已存在
|
||||
}
|
||||
try {
|
||||
await store.executeSql('ALTER TABLE events ADD COLUMN completed INTEGER DEFAULT 0');
|
||||
} catch (err) {
|
||||
// 列已存在
|
||||
}
|
||||
try {
|
||||
await store.executeSql("ALTER TABLE events ADD COLUMN rrule TEXT DEFAULT ''");
|
||||
} catch (err) {
|
||||
// 列已存在
|
||||
}
|
||||
try {
|
||||
await store.executeSql("ALTER TABLE events ADD COLUMN exdate TEXT DEFAULT ''");
|
||||
} catch (err) {
|
||||
// 列已存在
|
||||
}
|
||||
try {
|
||||
await store.executeSql('ALTER TABLE events ADD COLUMN reminder INTEGER DEFAULT 0');
|
||||
} catch (err) {
|
||||
// 列已存在
|
||||
}
|
||||
EventDb.db = store;
|
||||
return store;
|
||||
}
|
||||
|
||||
private static fromRow(rs: relationalStore.ResultSet): LocalEvent {
|
||||
const e = new LocalEvent();
|
||||
e.id = rs.getLong(rs.getColumnIndex('id'));
|
||||
e.uid = rs.getString(rs.getColumnIndex('uid'));
|
||||
e.calKey = rs.getString(rs.getColumnIndex('cal_key'));
|
||||
e.href = rs.getString(rs.getColumnIndex('href'));
|
||||
e.remotePath = rs.getString(rs.getColumnIndex('remote_path'));
|
||||
e.title = rs.getString(rs.getColumnIndex('title'));
|
||||
e.description = rs.getString(rs.getColumnIndex('description'));
|
||||
e.location = rs.getString(rs.getColumnIndex('location'));
|
||||
e.startTime = rs.getLong(rs.getColumnIndex('start_time'));
|
||||
e.endTime = rs.getLong(rs.getColumnIndex('end_time'));
|
||||
e.isAllDay = rs.getLong(rs.getColumnIndex('is_all_day')) === 1;
|
||||
e.etag = rs.getString(rs.getColumnIndex('etag'));
|
||||
e.dirty = rs.getLong(rs.getColumnIndex('dirty')) === 1;
|
||||
e.deleted = rs.getLong(rs.getColumnIndex('deleted')) === 1;
|
||||
e.recurring = rs.getLong(rs.getColumnIndex('recurring')) === 1;
|
||||
const kind: string = rs.getString(rs.getColumnIndex('kind'));
|
||||
e.kind = kind === '' ? 'event' : kind;
|
||||
e.completed = rs.getLong(rs.getColumnIndex('completed')) === 1;
|
||||
e.rrule = rs.getString(rs.getColumnIndex('rrule'));
|
||||
e.exdate = rs.getString(rs.getColumnIndex('exdate'));
|
||||
e.reminder = rs.getLong(rs.getColumnIndex('reminder'));
|
||||
return e;
|
||||
}
|
||||
|
||||
private static toBucket(e: LocalEvent): relationalStore.ValuesBucket {
|
||||
const bucket: relationalStore.ValuesBucket = {
|
||||
'uid': e.uid,
|
||||
'cal_key': e.calKey,
|
||||
'href': e.href,
|
||||
'remote_path': e.remotePath,
|
||||
'title': e.title,
|
||||
'description': e.description,
|
||||
'location': e.location,
|
||||
'start_time': e.startTime,
|
||||
'end_time': e.endTime,
|
||||
'is_all_day': e.isAllDay ? 1 : 0,
|
||||
'etag': e.etag,
|
||||
'dirty': e.dirty ? 1 : 0,
|
||||
'deleted': e.deleted ? 1 : 0,
|
||||
'recurring': e.recurring ? 1 : 0,
|
||||
'kind': e.kind,
|
||||
'completed': e.completed ? 1 : 0,
|
||||
'rrule': e.rrule,
|
||||
'exdate': e.exdate,
|
||||
'reminder': e.reminder
|
||||
};
|
||||
return bucket;
|
||||
}
|
||||
|
||||
/** 新建本地事件 */
|
||||
static async insertLocal(context: common.Context, e: LocalEvent): Promise<number> {
|
||||
const store = await EventDb.getDb(context);
|
||||
e.dirty = true;
|
||||
const rowId = await store.insert('events', EventDb.toBucket(e));
|
||||
return rowId;
|
||||
}
|
||||
|
||||
/** 更新本地事件(置 dirty 待推送) */
|
||||
static async updateLocal(context: common.Context, e: LocalEvent): Promise<void> {
|
||||
const store = await EventDb.getDb(context);
|
||||
e.dirty = true;
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.equalTo('id', e.id);
|
||||
await store.update(EventDb.toBucket(e), predicates);
|
||||
}
|
||||
|
||||
/** 标记删除(待推送 DELETE) */
|
||||
static async markDeleted(context: common.Context, id: number): Promise<void> {
|
||||
const store = await EventDb.getDb(context);
|
||||
const bucket: relationalStore.ValuesBucket = { 'deleted': 1, 'dirty': 1 };
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.equalTo('id', id);
|
||||
await store.update(bucket, predicates);
|
||||
}
|
||||
|
||||
/** 推送成功后清除 dirty(可回写 etag) */
|
||||
static async clearDirty(context: common.Context, id: number, etag: string): Promise<void> {
|
||||
const store = await EventDb.getDb(context);
|
||||
const bucket: relationalStore.ValuesBucket = { 'dirty': 0, 'etag': etag };
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.equalTo('id', id);
|
||||
await store.update(bucket, predicates);
|
||||
}
|
||||
|
||||
/** 推送删除成功后物理删除 */
|
||||
static async purge(context: common.Context, id: number): Promise<void> {
|
||||
const store = await EventDb.getDb(context);
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.equalTo('id', id);
|
||||
await store.delete(predicates);
|
||||
}
|
||||
|
||||
/** 所有待推送事件 */
|
||||
static async getDirty(context: common.Context): Promise<LocalEvent[]> {
|
||||
const store = await EventDb.getDb(context);
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.equalTo('dirty', 1);
|
||||
const rs = await store.query(predicates);
|
||||
const list: LocalEvent[] = [];
|
||||
try {
|
||||
while (rs.goToNextRow()) {
|
||||
list.push(EventDb.fromRow(rs));
|
||||
}
|
||||
} finally {
|
||||
rs.close();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 查询时间区间内未删除的日程(排除待办,待办单独展示) */
|
||||
static async queryRange(context: common.Context, start: number, end: number): Promise<LocalEvent[]> {
|
||||
const store = await EventDb.getDb(context);
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.equalTo('deleted', 0).and().equalTo('kind', 'event')
|
||||
.and().lessThanOrEqualTo('start_time', end)
|
||||
.and().greaterThanOrEqualTo('end_time', start);
|
||||
const rs = await store.query(predicates);
|
||||
const list: LocalEvent[] = [];
|
||||
try {
|
||||
while (rs.goToNextRow()) {
|
||||
list.push(EventDb.fromRow(rs));
|
||||
}
|
||||
} finally {
|
||||
rs.close();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 单个事件 */
|
||||
static async getById(context: common.Context, id: number): Promise<LocalEvent | null> {
|
||||
const store = await EventDb.getDb(context);
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.equalTo('id', id);
|
||||
const rs = await store.query(predicates);
|
||||
let result: LocalEvent | null = null;
|
||||
try {
|
||||
if (rs.goToNextRow()) {
|
||||
result = EventDb.fromRow(rs);
|
||||
}
|
||||
} finally {
|
||||
rs.close();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 全部未删除的待办(VTODO),未完成在前、按截止时间升序 */
|
||||
static async queryTodos(context: common.Context): Promise<LocalEvent[]> {
|
||||
const store = await EventDb.getDb(context);
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.equalTo('deleted', 0).and().equalTo('kind', 'todo');
|
||||
const rs = await store.query(predicates);
|
||||
const list: LocalEvent[] = [];
|
||||
try {
|
||||
while (rs.goToNextRow()) {
|
||||
list.push(EventDb.fromRow(rs));
|
||||
}
|
||||
} finally {
|
||||
rs.close();
|
||||
}
|
||||
list.sort((a: LocalEvent, b: LocalEvent): number => {
|
||||
if (a.completed !== b.completed) {
|
||||
return a.completed ? 1 : -1;
|
||||
}
|
||||
return a.startTime - b.startTime;
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 全部重复主事件(有 RRULE 且开始时间在 before 之前,用于跨窗口展开) */
|
||||
static async queryRecurringMasters(context: common.Context, before: number): Promise<LocalEvent[]> {
|
||||
const store = await EventDb.getDb(context);
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.equalTo('deleted', 0).and().equalTo('kind', 'event')
|
||||
.and().notEqualTo('rrule', '').and().lessThanOrEqualTo('start_time', before);
|
||||
const rs = await store.query(predicates);
|
||||
const list: LocalEvent[] = [];
|
||||
try {
|
||||
while (rs.goToNextRow()) {
|
||||
list.push(EventDb.fromRow(rs));
|
||||
}
|
||||
} finally {
|
||||
rs.close();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 未来 7 天内需要提醒的日程(reminder > 0),按开始时间升序 */
|
||||
static async queryRemindable(context: common.Context, from: number, to: number): Promise<LocalEvent[]> {
|
||||
const store = await EventDb.getDb(context);
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.equalTo('deleted', 0).and().equalTo('kind', 'event')
|
||||
.and().greaterThan('reminder', 0)
|
||||
.and().greaterThanOrEqualTo('start_time', from)
|
||||
.and().lessThanOrEqualTo('start_time', to)
|
||||
.orderByAsc('start_time').limitAs(50);
|
||||
const rs = await store.query(predicates);
|
||||
const list: LocalEvent[] = [];
|
||||
try {
|
||||
while (rs.goToNextRow()) {
|
||||
list.push(EventDb.fromRow(rs));
|
||||
}
|
||||
} finally {
|
||||
rs.close();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用远端数据刷新某个日历本(增量):
|
||||
* - etag 未变的跳过;变化的更新;远端没有的本地图删掉(排除本地待推送的新事件)
|
||||
* - 返回统计描述:"新增X 更新Y 删除Z 不变W"
|
||||
*/
|
||||
static async applyRemote(context: common.Context, calKey: string, href: string,
|
||||
remote: RemoteEvent[], isTodo: boolean): Promise<string> {
|
||||
const store = await EventDb.getDb(context);
|
||||
const kind: string = isTodo ? 'todo' : 'event';
|
||||
let added: number = 0;
|
||||
let updated: number = 0;
|
||||
let removed: number = 0;
|
||||
let unchanged: number = 0;
|
||||
// 读取该日历本当前所有行(仅同类型)
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.equalTo('cal_key', calKey).and().equalTo('kind', kind);
|
||||
const rs = await store.query(predicates);
|
||||
const existing: LocalEvent[] = [];
|
||||
try {
|
||||
while (rs.goToNextRow()) {
|
||||
existing.push(EventDb.fromRow(rs));
|
||||
}
|
||||
} finally {
|
||||
rs.close();
|
||||
}
|
||||
|
||||
const remoteKeys: string[] = [];
|
||||
for (const r of remote) {
|
||||
// 重复日程实例共享 UID,唯一标识 = uid + 开始时间
|
||||
const key: string = `${r.uid}_${r.startTime}`;
|
||||
remoteKeys.push(key);
|
||||
const found = existing.find((x: LocalEvent): boolean =>
|
||||
!x.dirty && x.uid === r.uid && x.startTime === r.startTime);
|
||||
if (found === undefined) {
|
||||
// 新增
|
||||
const e = new LocalEvent();
|
||||
e.uid = r.uid;
|
||||
e.calKey = calKey;
|
||||
e.href = href;
|
||||
e.remotePath = encodeURIComponent(r.uid) + '.ics';
|
||||
e.title = r.title;
|
||||
e.description = r.description;
|
||||
e.location = r.location;
|
||||
e.startTime = r.startTime;
|
||||
e.endTime = r.endTime;
|
||||
e.isAllDay = r.isAllDay;
|
||||
e.etag = r.etag;
|
||||
e.dirty = false;
|
||||
e.deleted = false;
|
||||
e.recurring = r.recurring;
|
||||
e.kind = kind;
|
||||
e.completed = r.completed;
|
||||
e.rrule = r.rrule;
|
||||
e.exdate = r.exdate;
|
||||
e.reminder = r.reminder;
|
||||
await store.insert('events', EventDb.toBucket(e));
|
||||
added++;
|
||||
} else if (found.etag !== r.etag || found.rrule !== r.rrule || found.exdate !== r.exdate
|
||||
|| found.reminder !== r.reminder) {
|
||||
// 更新(rrule/exdate 变化也更新,兼容老数据回填)
|
||||
found.title = r.title;
|
||||
found.description = r.description;
|
||||
found.location = r.location;
|
||||
found.startTime = r.startTime;
|
||||
found.endTime = r.endTime;
|
||||
found.isAllDay = r.isAllDay;
|
||||
found.etag = r.etag;
|
||||
found.dirty = false;
|
||||
found.deleted = false;
|
||||
found.recurring = r.recurring;
|
||||
found.kind = kind;
|
||||
found.completed = r.completed;
|
||||
found.rrule = r.rrule;
|
||||
found.exdate = r.exdate;
|
||||
found.reminder = r.reminder;
|
||||
const up = new relationalStore.RdbPredicates('events');
|
||||
up.equalTo('id', found.id);
|
||||
await store.update(EventDb.toBucket(found), up);
|
||||
updated++;
|
||||
} else {
|
||||
unchanged++;
|
||||
}
|
||||
}
|
||||
// 删除远端已不存在的(排除本地修改未推送的)
|
||||
for (const local of existing) {
|
||||
if (local.dirty) {
|
||||
continue;
|
||||
}
|
||||
const localKey: string = `${local.uid}_${local.startTime}`;
|
||||
if (!remoteKeys.includes(localKey)) {
|
||||
const del = new relationalStore.RdbPredicates('events');
|
||||
del.equalTo('id', local.id);
|
||||
await store.delete(del);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
return `新增${added} 更新${updated} 删除${removed} 不变${unchanged}`;
|
||||
}
|
||||
|
||||
/** 删除某账号全部本地事件(删除账号时调用) */
|
||||
static async deleteAccountEvents(context: common.Context, accId: string): Promise<void> {
|
||||
const store = await EventDb.getDb(context);
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.like(`cal_key`, `${accId}%`);
|
||||
await store.delete(predicates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新勾选日历本后清理失效数据:
|
||||
* 删除该账号下 calKey 不在有效列表中的本地日程/待办(calKey = accId_序号,重选后序号会变)
|
||||
*/
|
||||
static async pruneAccountEvents(context: common.Context, accId: string,
|
||||
validCalKeys: string[]): Promise<void> {
|
||||
const store = await EventDb.getDb(context);
|
||||
const predicates = new relationalStore.RdbPredicates('events');
|
||||
predicates.like('cal_key', `${accId}_%`);
|
||||
if (validCalKeys.length > 0) {
|
||||
predicates.and().notIn('cal_key', validCalKeys);
|
||||
}
|
||||
await store.delete(predicates);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user