项目文件夹

文件
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

200 行
6.6 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
import AlarmManagerService, {
type AlarmClockInfo,
type AlarmRepeatMode,
} from '../../os/AlarmManagerService';
import BroadcastBus, {
ACTION_BOOT_COMPLETED,
ACTION_TIME_SET,
ACTION_TIME_TICK,
} from '../../os/BroadcastBus';
import { createAppStoreWithActions, memoSelector, registerStateAdapter } from '../../os/createAppStore';
import * as TimeService from '../../os/TimeService';
import { CLOCK_CONFIG } from './data';
import type { Alarm, WorldCity } from './types';
import { getNextTrigger } from './utils';
// ---- Types ----
interface ClockState {
alarms: Alarm[];
selectedCityIds: string[];
}
interface ClockActions {
/** Set alarms array directly, or via updater function */
setAlarms: (updater: Alarm[] | ((prev: Alarm[]) => Alarm[])) => void;
/** Toggle a single alarm's enabled state */
toggleAlarm: (id: string) => void;
/** Add or update an alarm (upsert by id) */
saveAlarm: (alarm: Alarm) => void;
/** Delete alarms by a set of IDs */
deleteAlarms: (ids: Set<string>) => void;
/** Set selectedCityIds directly, or via updater function */
setSelectedCityIds: (updater: string[] | ((prev: string[]) => string[])) => void;
/** Add a city to the selected list (no-op if already present) */
addCity: (cityId: string) => void;
/** Remove cities by a set of IDs */
removeCities: (ids: Set<string>) => void;
}
// ---- Store ----
const initialState: ClockState = {
alarms: CLOCK_CONFIG.alarms as Alarm[],
selectedCityIds: CLOCK_CONFIG.selectedCityIds,
};
export const useClockStore = createAppStoreWithActions<ClockState, ClockActions>(
'clock',
initialState,
(set, get) => ({
setAlarms: (updater) => {
set(state => ({
alarms: typeof updater === 'function' ? updater(state.alarms) : updater,
}));
},
toggleAlarm: (id) => {
set(state => ({
alarms: state.alarms.map(a => (a.id === id ? { ...a, enabled: !a.enabled } : a)),
}));
},
saveAlarm: (alarm) => {
set(state => {
const idx = state.alarms.findIndex(a => a.id === alarm.id);
if (idx >= 0) {
const next = [...state.alarms];
next[idx] = alarm;
return { alarms: next };
}
return { alarms: [alarm, ...state.alarms] };
});
},
deleteAlarms: (ids) => {
set(state => ({
alarms: state.alarms.filter(a => !ids.has(a.id)),
}));
},
setSelectedCityIds: (updater) => {
set(state => ({
selectedCityIds: typeof updater === 'function' ? updater(state.selectedCityIds) : updater,
}));
},
addCity: (cityId) => {
set(state => {
if (state.selectedCityIds.includes(cityId)) return {};
return { selectedCityIds: [...state.selectedCityIds, cityId] };
});
},
removeCities: (ids) => {
set(state => ({
selectedCityIds: state.selectedCityIds.filter(id => !ids.has(id)),
}));
},
}),
);
// ---- Memoized Selectors ----
type ClockStore = ClockState & ClockActions;
/** Derive the full WorldCity objects from selectedCityIds */
export const selectSelectedCities = memoSelector(
(s: ClockStore) => s.selectedCityIds,
(ids: string[]): WorldCity[] => CLOCK_CONFIG.cities.filter(city => ids.includes(city.id)),
);
// ---- AlarmManagerService publisher ─────────────────────────────────
// Real Android: Clock publishes alarms to AlarmManager via setAlarmClock();
// any consumer (lockscreen / status bar / MAML widget) reads next-alarm via
// getNextAlarmClock() — system never knows which app set it. mobile-gym
// mirrors this in os/AlarmManagerService so the widget ambient adapter is
// decoupled from this app's private store.
const CLOCK_PACKAGE_NAME = 'com.android.deskclock';
function alarmRepeatToMode(repeat: Alarm['repeat']): AlarmRepeatMode {
switch (repeat) {
case 'daily':
case 'workday':
case 'weekday':
case 'holiday':
case 'once':
return repeat;
default:
return 'custom';
}
}
function alarmToAlarmClockInfo(alarm: Alarm): AlarmClockInfo {
return {
id: alarm.id,
ownerPackage: CLOCK_PACKAGE_NAME,
hour: alarm.hour,
minute: alarm.minute,
label: alarm.note ?? '',
repeat: alarmRepeatToMode(alarm.repeat),
triggerAtMs: getNextTrigger(alarm, TimeService.getDate()),
};
}
function publishAlarmsToManager(alarms: Alarm[]): void {
const enabled = alarms.filter((a) => a.enabled);
AlarmManagerService.setAlarmClocksFor(
CLOCK_PACKAGE_NAME,
enabled.map(alarmToAlarmClockInfo),
);
}
function republishCurrentAlarms(): void {
publishAlarmsToManager(useClockStore.getState().alarms);
}
// Initial publish on module load (covers cold-start when defaults already
// have enabled alarms).
republishCurrentAlarms();
useClockStore.subscribe((state, prev) => {
if (state.alarms === prev.alarms) return;
publishAlarmsToManager(state.alarms);
});
// triggerAtMs is computed relative to "now", so it goes stale as wall-clock
// time advances or jumps. Re-publish (recomputing every alarm's next trigger
// against the current TimeService time) whenever time moves so the widget's
// next_alarm_time / clock_alarmtime never serve expired data:
// - TIME_TICK fires each minute while simulated time flows (covers an alarm
// time passing during normal playback). setAlarmClocksFor dedups, so ticks
// that don't cross a trigger boundary are no-ops.
// - TIME_SET fires on __SIM_TIME__.setSimulatedTime() jumps.
BroadcastBus.registerReceiver(ACTION_TIME_TICK, republishCurrentAlarms);
BroadcastBus.registerReceiver(ACTION_TIME_SET, republishCurrentAlarms);
// __SIM__.resetState() (no page reload) wipes the volatile AlarmManager store
// AFTER app stores re-init, leaving the manager empty until the next alarm
// change. OSContext re-emits BOOT_COMPLETED at the end of the reset so derived
// volatile services (this + MediaSession) re-publish from the restored store.
BroadcastBus.registerReceiver(ACTION_BOOT_COMPLETED, republishCurrentAlarms);
// ---- State Adapter (bench_env) ----
// 补齐旧 API 暴露的 alarms / selectedCitiesApp 未挂载时从 CONFIG 默认值计算)
registerStateAdapter('clock', (state) => {
const result = { ...state };
result.cities = state.cities ?? CLOCK_CONFIG.cities;
if (!Array.isArray(result.alarms)) {
result.alarms = state.alarms ?? CLOCK_CONFIG.alarms;
}
if (!Array.isArray(result.selectedCities)) {
const ids: string[] = state.selectedCityIds ?? CLOCK_CONFIG.selectedCityIds;
const cities: any[] = result.cities;
result.selectedCities = cities.filter((c: any) => ids.includes(c.id));
}
return result;
});