package com.jsbogang.womp.health; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.Looper; import android.os.PowerManager; import android.util.Log; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import com.inuker.bluetooth.library.Code; import com.jsbogang.womp.ble.AlarmPrefs; import com.jsbogang.womp.ble.DevicePrefs; import com.jsbogang.womp.ble.VeepooBleManager; import com.veepoo.protocol.VPOperateManager; import com.veepoo.protocol.listener.data.IOriginData3Listener; import com.veepoo.protocol.listener.data.ISleepDataListener; import com.veepoo.protocol.model.datas.HRVOriginData; import com.veepoo.protocol.model.datas.OriginData3; import com.veepoo.protocol.model.datas.OriginHalfHourData; import com.veepoo.protocol.model.datas.SleepData; import com.veepoo.protocol.model.datas.Spo2hOriginData; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Queue; public final class HealthSyncer { public static final String ACTION_HEALTH = "com.jsbogang.womp.ACTION_HEALTH"; public static final String EXTRA_MSG = "msg"; private static final String TAG = "HealthSyncer"; private static final HealthSyncer INSTANCE = new HealthSyncer(); private Context app; private final Handler handler = new Handler(Looper.getMainLooper()); private volatile boolean busy; private volatile boolean sleepBusy; private PowerManager.WakeLock wakeLock; private final Queue dayQueue = new LinkedList<>(); private boolean sleepAfter; private HealthSyncer() {} public static HealthSyncer get() { return INSTANCE; } public void attach(Context context) { app = context.getApplicationContext(); } public boolean isSyncing() { return busy; } private void setBusy(boolean value) { busy = value; if (value) { holdWakeLock(); } else { dropWakeLock(); } } private void holdWakeLock() { if (app == null) { return; } try { if (wakeLock == null) { PowerManager pm = (PowerManager) app.getSystemService(Context.POWER_SERVICE); wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "womp:origin_sync"); wakeLock.setReferenceCounted(false); } if (!wakeLock.isHeld()) { wakeLock.acquire(3 * 60_000L); } } catch (Exception ignored) { } } private void dropWakeLock() { try { if (wakeLock != null && wakeLock.isHeld()) { wakeLock.release(); } } catch (Exception ignored) { } } public void syncAll() { syncAll(true); } public void syncAll(boolean notifyOffline) { if (app == null || !VeepooBleManager.get().isReady()) { if (notifyOffline) { notifyUi("设备未连接,无法同步"); } return; } if (busy) { notifyUi("正在同步中…"); return; } if (ActiveMeasureRunner.get().isMeasuring()) { notifyUi("正在测量中,稍后再同步"); return; } int watchDay = DevicePrefs.getWatchDay(app); dayQueue.clear(); int n = Math.min(Math.max(watchDay, 1), 7); for (int d = 0; d < n; d++) { dayQueue.add(d); } sleepAfter = true; next(); } public void syncToday() { if (app == null || !VeepooBleManager.get().isReady() || busy || ActiveMeasureRunner.get().isMeasuring()) { return; } dayQueue.clear(); dayQueue.add(0); sleepAfter = false; next(); } private void next() { Integer day = dayQueue.poll(); if (day == null) { if (sleepAfter) { sleepAfter = false; readSleep(true); } else { setBusy(false); refreshLiveStep(); HealthUploader.get().flush(); notifyUi("同步完成,之后每5分钟自动拉取"); } return; } String mac = DevicePrefs.getMac(app); String date = dateOf(day); Integer cursor = AppDatabase.dao().cursorOf(mac, date); int position = cursor == null ? 1 : cursor + 1; if (position > 288) { next(); return; } setBusy(true); int watchDay = DevicePrefs.getWatchDay(app); notifyUi("读取 " + date + " 从第 " + position + " 条"); VPOperateManager.getInstance().readOriginDataSingleDay(code -> { if (code != Code.REQUEST_SUCCESS) { Log.w(TAG, "read origin write fail " + code); } }, new IOriginData3Listener() { @Override public void onOriginFiveMinuteListDataChange(List originDataList) { ingest(mac, date, originDataList); } @Override public void onOriginHalfHourDataChange(OriginHalfHourData originHalfHourData) { } @Override public void onOriginHRVOriginListDataChange(List originHrvDataList) { } @Override public void onOriginSpo2OriginListDataChange(List originSpo2hDataList) { } @Override public void onReadOriginProgress(float progress) { } @Override public void onReadOriginProgressDetail(int d, String dateStr, int allPackage, int currentPackage) { } @Override public void onReadOriginComplete() { handler.post(() -> { setBusy(false); next(); }); } }, day, position, watchDay); } private void ingest(String mac, String date, List list) { if (list == null || list.isEmpty()) { return; } HealthDao dao = AppDatabase.dao(); int maxPos = 0; for (OriginData3 item : list) { HealthSample sample = OriginMapper.toSample(item, mac, date); if (sample == null) { continue; } HealthSample old = dao.find(sample.timestamp, mac); if (old != null) { if (sample.heartRate <= 0) { sample.heartRate = old.heartRate; } if (sample.bpHigh <= 0) { sample.bpHigh = old.bpHigh; sample.bpLow = old.bpLow; } if (sample.spo2 <= 0) { sample.spo2 = old.spo2; } if (sample.glucose <= 0) { sample.glucose = old.glucose; } } if (AlarmPrefs.isAbnormal(app, sample.heartRate, sample.bpHigh, sample.bpLow, sample.spo2)) { sample.dataType = "异常"; } if (old != null && old.uploaded == 1 && "周期".equals(old.dataType) && "周期".equals(sample.dataType)) { sample.uploaded = 1; } dao.upsertSample(sample); if (sample.packageNumber > maxPos) { maxPos = sample.packageNumber; } } if (maxPos > 0) { SyncCursor c = new SyncCursor(); c.deviceMac = mac; c.dayDate = date; Integer prev = dao.cursorOf(mac, date); c.lastPosition = Math.max(maxPos, prev == null ? 0 : prev); dao.upsertCursor(c); } notifyUi("已入库 " + list.size() + " 条"); HealthUploader.get().flush(); } public void syncSleep() { if (app == null || !VeepooBleManager.get().isReady() || busy || sleepBusy) { return; } readSleep(false); } private void readSleep(boolean fullSync) { if (fullSync) { setBusy(true); } else { sleepBusy = true; } int watchDay = DevicePrefs.getWatchDay(app); if (!fullSync) { watchDay = Math.min(Math.max(watchDay, 1), 2); } String mac = DevicePrefs.getMac(app); VPOperateManager.getInstance().readSleepData(code -> {}, new ISleepDataListener() { @Override public void onSleepDataChange(String day, SleepData sleepData) { SleepSession s = OriginMapper.toSleep(sleepData, mac, day); if (s != null && s.date != null && s.allMinutes > 0) { AppDatabase.dao().upsertSleep(s); } } @Override public void onSleepProgress(float progress) { } @Override public void onSleepProgressDetail(String day, int packagenumber) { } @Override public void onReadSleepComplete() { handler.post(() -> { if (fullSync) { setBusy(false); refreshLiveStep(); HealthUploader.get().flush(); notifyUi("同步完成,之后每5分钟自动拉取"); } else { sleepBusy = false; notifyUi(null); } }); } }, watchDay); } private String dateOf(int dayOffset) { Calendar c = Calendar.getInstance(); c.add(Calendar.DAY_OF_YEAR, -dayOffset); return new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA).format(c.getTime()); } void notifyUi(String msg) { if (app == null) { return; } Intent i = new Intent(ACTION_HEALTH); if (msg != null) { i.putExtra(EXTRA_MSG, msg); } LocalBroadcastManager.getInstance(app).sendBroadcast(i); } public void refreshLiveStep() { if (app == null || !VeepooBleManager.get().isReady()) { return; } try { VPOperateManager.getInstance().readSportStep(code -> {}, sportData -> { if (sportData != null && sportData.getStep() >= 0) { DevicePrefs.setLiveStep(app, sportData.getStep()); notifyUi(null); } }); } catch (Exception ignored) { } } /** 当天步数:取当天记录到的最大步数;今天再与手表实时计步取较大值。 */ public static int daySteps(Context context, String mac, long from, long to) { if (AppDatabase.get() == null || mac == null) { return 0; } Integer max = AppDatabase.dao().maxSteps(mac, from, to); int m = max == null ? 0 : max; long todayStart = startOfDay(new Date()); if (from == todayStart && context != null) { int live = DevicePrefs.getLiveStep(context); if (live > m) { return live; } } return m; } public static long startOfDay(Date date) { Calendar c = Calendar.getInstance(); c.setTime(date); c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); return c.getTimeInMillis(); } }