Files
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 lines
6.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
});