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<Integer> 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<OriginData3> originDataList) {
|
ingest(mac, date, originDataList);
|
}
|
|
@Override
|
public void onOriginHalfHourDataChange(OriginHalfHourData originHalfHourData) {
|
}
|
|
@Override
|
public void onOriginHRVOriginListDataChange(List<HRVOriginData> originHrvDataList) {
|
}
|
|
@Override
|
public void onOriginSpo2OriginListDataChange(List<Spo2hOriginData> 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<OriginData3> 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();
|
}
|
}
|