- 通知设置页面新增 HRV 压力提醒开关,支持自定义开启或关闭压力监测推送 - 锻炼详情页集成分享功能,支持将运动数据生成精美长图并分享 - 优化 HRV 监测服务逻辑,在发送通知前检查用户偏好设置 - 更新多语言配置文件,添加相关文案翻译 - 将应用版本号更新至 1.1.5
178 lines
5.5 KiB
TypeScript
178 lines
5.5 KiB
TypeScript
import { analyzeHRVData, fetchHRVWithStatus } from '@/utils/health';
|
||
import AsyncStorage from '@/utils/kvStore';
|
||
import { logger } from '@/utils/logger';
|
||
import { convertHrvToStressIndex, getStressLevelInfo, StressLevel } from '@/utils/stress';
|
||
import { getHRVReminderEnabled, getNotificationEnabled } from '@/utils/userPreferences';
|
||
import { NativeEventEmitter, NativeModules } from 'react-native';
|
||
import { sendHRVStressNotification } from './hrvNotificationService';
|
||
|
||
const { HealthKitManager } = NativeModules;
|
||
|
||
const HRV_EVENT_NAME = 'hrvUpdate';
|
||
const HRV_NOTIFICATION_STATE_KEY = '@hrv_stress_notification_state';
|
||
const MIN_NOTIFICATION_INTERVAL_HOURS = 2;
|
||
|
||
interface HrvEventData {
|
||
timestamp: number;
|
||
type: 'hrv_data_updated' | string;
|
||
}
|
||
|
||
interface NotificationState {
|
||
lastSentAt: number;
|
||
lastStressLevel: StressLevel;
|
||
}
|
||
|
||
class HRVMonitorService {
|
||
private eventEmitter: NativeEventEmitter | null = null;
|
||
private eventSubscription: any = null;
|
||
private isInitialized = false;
|
||
private lastProcessedTime = 0;
|
||
private debounceDelay = 3000;
|
||
|
||
async initialize(): Promise<boolean> {
|
||
if (this.isInitialized) {
|
||
logger.info('[HRVMonitor] Already initialized');
|
||
return true;
|
||
}
|
||
|
||
try {
|
||
this.eventEmitter = new NativeEventEmitter(HealthKitManager);
|
||
this.eventSubscription = this.eventEmitter.addListener(
|
||
HRV_EVENT_NAME,
|
||
this.handleHRVUpdate.bind(this)
|
||
);
|
||
|
||
if (HealthKitManager.startHRVObserver) {
|
||
await HealthKitManager.startHRVObserver();
|
||
} else {
|
||
logger.warn('[HRVMonitor] Native startHRVObserver not available');
|
||
return false;
|
||
}
|
||
|
||
this.isInitialized = true;
|
||
logger.info('[HRVMonitor] Initialized successfully');
|
||
return true;
|
||
} catch (error) {
|
||
logger.error('[HRVMonitor] Initialization failed:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async stop(): Promise<void> {
|
||
if (!this.isInitialized) return;
|
||
|
||
try {
|
||
if (this.eventSubscription) {
|
||
this.eventSubscription.remove();
|
||
this.eventSubscription = null;
|
||
}
|
||
|
||
if (HealthKitManager.stopHRVObserver) {
|
||
await HealthKitManager.stopHRVObserver();
|
||
}
|
||
|
||
this.isInitialized = false;
|
||
logger.info('[HRVMonitor] Stopped successfully');
|
||
} catch (error) {
|
||
logger.error('[HRVMonitor] Failed to stop:', error);
|
||
}
|
||
}
|
||
|
||
private async handleHRVUpdate(event: HrvEventData): Promise<void> {
|
||
logger.info('[HRVMonitor] HRV data updated:', event);
|
||
|
||
const now = Date.now();
|
||
if (now - this.lastProcessedTime < this.debounceDelay) {
|
||
logger.info('[HRVMonitor] Debouncing HRV update');
|
||
return;
|
||
}
|
||
this.lastProcessedTime = now;
|
||
|
||
try {
|
||
const [notificationsEnabled, hrvReminderEnabled] = await Promise.all([
|
||
getNotificationEnabled(),
|
||
getHRVReminderEnabled(),
|
||
]);
|
||
|
||
if (!notificationsEnabled || !hrvReminderEnabled) {
|
||
logger.info('[HRVMonitor] Notification preference disabled, skip HRV push');
|
||
return;
|
||
}
|
||
|
||
const canNotify = await this.canSendNotification(now);
|
||
if (!canNotify) {
|
||
logger.info('[HRVMonitor] Notification recently sent, skipping');
|
||
return;
|
||
}
|
||
|
||
const { hrvData, message } = await fetchHRVWithStatus(new Date());
|
||
if (!hrvData) {
|
||
logger.warn('[HRVMonitor] No HRV data available for analysis');
|
||
return;
|
||
}
|
||
|
||
const analysis = analyzeHRVData(hrvData);
|
||
const stressIndex = convertHrvToStressIndex(hrvData.value);
|
||
if (stressIndex == null) {
|
||
logger.warn('[HRVMonitor] Unable to derive stress index from HRV value:', hrvData.value);
|
||
return;
|
||
}
|
||
|
||
const stressInfo = getStressLevelInfo(stressIndex);
|
||
await sendHRVStressNotification({
|
||
hrvValue: Math.round(hrvData.value),
|
||
stressIndex,
|
||
stressLevel: stressInfo.level,
|
||
recordedAt: hrvData.recordedAt,
|
||
interpretation: analysis.interpretation,
|
||
recommendations: analysis.recommendations,
|
||
dataSource: analysis.dataSource,
|
||
message,
|
||
});
|
||
|
||
await this.markNotificationSent(now, stressInfo.level);
|
||
} catch (error) {
|
||
logger.error('[HRVMonitor] Failed to process HRV update:', error);
|
||
}
|
||
}
|
||
|
||
private async canSendNotification(now: number): Promise<boolean> {
|
||
try {
|
||
const stored = await AsyncStorage.getItem(HRV_NOTIFICATION_STATE_KEY);
|
||
if (!stored) {
|
||
return true;
|
||
}
|
||
|
||
const state: NotificationState = JSON.parse(stored);
|
||
const elapsed = now - state.lastSentAt;
|
||
const minIntervalMs = MIN_NOTIFICATION_INTERVAL_HOURS * 60 * 60 * 1000;
|
||
|
||
// 只检查最小间隔时间(2小时),不再限制每天只能发送一次
|
||
if (elapsed < minIntervalMs) {
|
||
const hoursLeft = ((minIntervalMs - elapsed) / (1000 * 60 * 60)).toFixed(1);
|
||
logger.info(`[HRVMonitor] Cooldown active, ${hoursLeft}h remaining`);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
logger.warn('[HRVMonitor] Failed to read notification state:', error);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private async markNotificationSent(timestamp: number, level: StressLevel): Promise<void> {
|
||
try {
|
||
const state: NotificationState = {
|
||
lastSentAt: timestamp,
|
||
lastStressLevel: level,
|
||
};
|
||
await AsyncStorage.setItem(HRV_NOTIFICATION_STATE_KEY, JSON.stringify(state));
|
||
} catch (error) {
|
||
logger.warn('[HRVMonitor] Failed to persist notification state:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
export const hrvMonitorService = new HRVMonitorService();
|