wangzhibo
4 天以前 ae6f40460dcd56af6c5f60ba52c883854c3bac55
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
package nodomain.freeyourgadget.gadgetbridge.service;
 
import android.content.Context;
import android.content.Intent;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.util.ArrayList;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
 
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.SleepAsAndroidFeature;
import nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid.SleepAsAndroidAction;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
 
public class SleepAsAndroidSender {
 
    private final Logger LOG = LoggerFactory.getLogger(SleepAsAndroidSender.class);
    private final String PACKAGE_SLEEP_AS_ANDROID = "com.urbandroid.sleep";
    private final String ACTION_EXTRA_DATA_UPDATE = "com.urbandroid.sleep.ACTION_EXTRA_DATA_UPDATE";
    private final String ACTION_MOVEMENT_DATA_UPDATE = "com.urbandroid.sleep.watch.DATA_UPDATE";
    private final String ACTION_HEART_RATE_DATA_UPDATE = "com.urbandroid.sleep.watch.HR_DATA_UPDATE";
    private final String ACTION_RESUME_FROM_WATCH = "com.urbandroid.sleep.watch.RESUME_FROM_WATCH";
    private final String ACTION_PAUSE_FROM_WATCH = "com.urbandroid.sleep.watch.PAUSE_FROM_WATCH";
    private final String ACTION_SNOOZE_FROM_WATCH = "com.urbandroid.sleep.watch.SNOOZE_FROM_WATCH";
    private final String ACTION_DISMISS_FROM_WATCH = "com.urbandroid.sleep.watch.DISMISS_FROM_WATCH";
 
    private final String MAX_RAW_DATA = "MAX_RAW_DATA";
    private final String DATA = "DATA";
    private final String EXTRA_DATA_HR = "com.urbandroid.sleep.EXTRA_DATA_HR";
    private final String EXTRA_DATA_RR = "com.urbandroid.sleep.EXTRA_DATA_RR";
    private final String EXTRA_DATA_SPO2 = "com.urbandroid.sleep.EXTRA_DATA_SPO2";
    private final String EXTRA_DATA_SDNN = "com.urbandroid.sleep.EXTRA_DATA_SDNN";
    private final String EXTRA_DATA_TIMESTAMP = "com.urbandroid.sleep.EXTRA_DATA_TIMESTAMP";
    private final String EXTRA_DATA_FRAMERATE = "com.urbandroid.sleep.EXTRA_DATA_FRAMERATE";
    private final String EXTRA_DATA_BATCH = "com.urbandroid.sleep.EXTRA_DATA_BATCH";
 
 
    private GBDevice device;
    private boolean trackingOngoing = false;
    private boolean trackingPaused = false;
 
    private ScheduledExecutorService trackingPauseScheduler;
    private long batchSize = 1;
    private long lastRawDataMs = 0;
    private float maxRawData = 0;
    private long lastHrDataMs = 0;
    private ArrayList<Float> hrData = new ArrayList<>();
 
    private ArrayList<Float> accData = new ArrayList<>();
    private ScheduledExecutorService accDataScheduler;
    private Set<SleepAsAndroidFeature> features;
 
    public SleepAsAndroidSender(GBDevice gbDevice) {
        this.device = gbDevice;
        this.features = gbDevice.getDeviceCoordinator().getSleepAsAndroidFeatures();
    }
 
    /**
     * Check if a SleepAsAndroid feature is enabled
     *
     * @param feature the feature
     * @return true if the feature is enabled
     */
    public boolean hasFeature(SleepAsAndroidFeature feature) {
        return this.features.contains(feature);
    }
 
    /**
     * Check if a SleepAsAndroid feature is enabled
     * @param feature
     * @return
     */
    public boolean isFeatureEnabled(SleepAsAndroidFeature feature) {
        boolean enabled = isSleepAsAndroidEnabled();
        if (enabled) {
            switch (feature) {
                case ACCELEROMETER:
                    enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_movement", false);
                    break;
                case HEART_RATE:
                    enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_hr", false);
                    break;
                case SPO2:
                    enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_spo2", false);
                    break;
                case OXIMETRY:
                    enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_oximetry", false);
                    break;
                case NOTIFICATIONS:
                    enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_notifications", false);
                    break;
                case ALARMS:
                    enabled = GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_feat_alarms", false);
                    break;
                default:
                    break;
            }
        }
        return enabled;
    }
 
    /**
     * Get all enabled features
     *
     * @return all enabled features
     */
    public Set<SleepAsAndroidFeature> getFeatures() {
        return features;
    }
 
    /**
     * Start tracking
     */
    public void startTracking() {
        if (!isDeviceDefault()) return;
 
        stopTracking();
 
        accDataScheduler = Executors.newSingleThreadScheduledExecutor();
        accDataScheduler.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                aggregateAndSendAccelData();
            }
        }, 9999, 9999, TimeUnit.MILLISECONDS);
 
        lastRawDataMs = System.currentTimeMillis();
        lastHrDataMs = System.currentTimeMillis();
 
        this.trackingOngoing = true;
    }
 
    /**
     * Stop tracking
     */
    public void stopTracking() {
        if (!isDeviceDefault() || !trackingOngoing) return;
        if (accDataScheduler != null) {
            accDataScheduler.shutdownNow();
            accDataScheduler = null;
        }
 
        this.trackingOngoing = false;
        this.hrData = new ArrayList<>();
        this.accData = new ArrayList<>();
        this.lastHrDataMs = 0;
        this.lastRawDataMs = 0;
    }
 
    /**
     * Pause tracking
     *
     * @param timeout the timeout in milliseconds before resuming
     */
    public void pauseTracking(long timeout) {
        if (!isDeviceDefault() || !trackingOngoing) return;
 
        if (timeout <= 0) {
            resumeTracking();
            return;
        }
 
        pauseTracking();
        trackingPauseScheduler = setPauseTracking(timeout);
    }
 
    /**
     * Same as {@link #pauseTracking(long)} but pausing and resuming is controlled by a toggle
     *
     * @param suspended true if the tracking should be paused, false if should be resumed
     */
    public void pauseTracking(boolean suspended) {
        if (!isDeviceDefault() || !trackingOngoing) return;
        trackingPaused = suspended;
        if (!trackingPaused) {
            resumeTracking();
            return;
        }
    }
 
    /**
     * Set a scheduler to resume tracking
     *
     * @param delay the delay
     * @return the scheduler
     */
    private ScheduledExecutorService setPauseTracking(long delay) {
        trackingPaused = true;
        trackingPauseScheduler = Executors.newSingleThreadScheduledExecutor();
        trackingPauseScheduler.schedule(new Runnable() {
            @Override
            public void run() {
                resumeTracking();
            }
        }, delay, TimeUnit.MILLISECONDS);
 
        return trackingPauseScheduler;
    }
 
    /**
     * Pause tracking
     */
    private void pauseTracking() {
        if (!isDeviceDefault() && trackingPaused) return;
        trackingPaused = true;
        stopTracking();
    }
 
    /**
     * Resume tracking
     */
    private void resumeTracking() {
        if (!isDeviceDefault() && !trackingPaused) return;
        if (trackingPauseScheduler != null) {
            trackingPauseScheduler.shutdownNow();
            trackingPauseScheduler = null;
        }
        trackingPaused = false;
        startTracking();
    }
 
    /**
     * Set the batch size
     *
     * @param batchSize the batch size
     */
    public void setBatchSize(long batchSize) {
        if (!isDeviceDefault()) return;
        LOG.debug("Setting batch size to " + batchSize);
        this.batchSize = batchSize;
    }
 
    /**
     * Confirm that the device is connected
     */
    public void confirmConnected() {
        if (!isDeviceDefault()) return;
        LOG.debug("Confirming connected");
        Intent intent = new Intent(SleepAsAndroidAction.CONFIRM_CONNECTED);
        broadcastToSleepAsAndroid(intent);
    }
 
    /**
     * On accelerometer changed
     *
     * @param x the x value
     * @param y the y value
     * @param z the z value
     */
    public void onAccelChanged(float x, float y, float z) {
        if (!isDeviceDefault() || !isFeatureEnabled(SleepAsAndroidFeature.ACCELEROMETER) || !hasFeature(SleepAsAndroidFeature.ACCELEROMETER) || !trackingOngoing)
            return;
        if (trackingPaused)
            return;
 
        updateMaxRawData(x, y, z);
    }
 
    /**
     * Aggregate and send the acceleration data
     */
    private synchronized void aggregateAndSendAccelData() {
        if (!trackingOngoing || trackingPaused) return;
        if (maxRawData > 0) {
            accData.add(maxRawData);
            maxRawData = 0;
            if (accData.size() == batchSize) {
                sendAccelData();
            }
        }
    }
 
    /**
     * Send the acceleration data
     */
    private void sendAccelData() {
        LOG.debug("Sending movement data: " + this.accData + " batch size: " + batchSize + " array size: " + accData.size());
        Intent intent = new Intent(ACTION_MOVEMENT_DATA_UPDATE);
        intent.putExtra(MAX_RAW_DATA, convertToFloatArray(this.accData));
        accData.clear();
        broadcastToSleepAsAndroid(intent);
    }
 
    /**
     * Update the max raw data
     * @param x the x value
     * @param y the y value
     * @param z the z value
     */
    private void updateMaxRawData(float x, float y, float z) {
        float maxRaw = calculateAccelerationMagnitude(x, y, z);
        if (maxRaw > maxRawData) {
            maxRawData = maxRaw;
        }
    }
 
    /**
     * Calculate the acceleration magnitude
     * @param x the x value
     * @param y the y value
     * @param z the z value
     * @return
     */
    protected float calculateAccelerationMagnitude(float x, float y, float z) {
        double sqrt = Math.sqrt((x * x) + (y * y) + (z * z));
        return (float)sqrt;
    }
 
    /**
     * On heart rate changed
     *
     * @param hr        the heart rate
     * @param sendDelay the send delay in ms. If 0 the data will be send right away. Anything bigger will gather all the data then send it all after the specified interval
     */
    public void onHrChanged(float hr, long sendDelay) {
        if (!isDeviceDefault() || !isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE) || !hasFeature(SleepAsAndroidFeature.HEART_RATE) || !trackingOngoing)
            return;
        if (trackingPaused) return;
 
        updateLastHrData(hr);
 
        if (lastHrDataMs == 0) {
            lastHrDataMs = System.currentTimeMillis();
        }
        long ms = System.currentTimeMillis();
        if (ms - lastHrDataMs >= sendDelay) {
            lastHrDataMs = ms;
            sendHrData();
        }
    }
 
    /**
     * Send the heart rate data
     */
    private synchronized void sendHrData() {
        LOG.debug("Sending heart rate data: " + this.hrData);
        Intent intent = new Intent(ACTION_HEART_RATE_DATA_UPDATE);
        intent.putExtra(DATA, convertToFloatArray(this.hrData));
        broadcastToSleepAsAndroid(intent);
        this.hrData.clear();
    }
 
    /**
     * Update the last heart rate data
     * @param hr the heart rate
     */
    private void updateLastHrData(float hr) {
        this.hrData.add(hr);
    }
 
    /**
     * This is a generic intent to carry data from various sensors.
     * See Sleep As Android documentation for parameters values.
     * <a href="https://docs.sleep.urbandroid.org/devs/wearable_api.html#send-various-body-sensors-data-hr-rr-spo2-sdnn">...</a>
     */
    public synchronized void sendExtra(Float hr, Long extraDataTimestamp, Long extraDataFramerate, ArrayList<Float> extraDataRR, ArrayList<Float> spo2Batch, Float sdnn, ArrayList<Float> extraDataBatch) {
 
        if (!isDeviceDefault() || !trackingOngoing) return;
        if (trackingPaused) return;
        Context context = GBApplication.getContext();
        Intent intent = new Intent(ACTION_EXTRA_DATA_UPDATE);
 
        // Heart Rate
        if (hr != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
            intent.putExtra(EXTRA_DATA_HR, hr);
        }
 
        // SpO2
        if (spo2Batch != null && (hasFeature(SleepAsAndroidFeature.SPO2) && isFeatureEnabled(SleepAsAndroidFeature.SPO2))) {
            intent.putExtra(EXTRA_DATA_SPO2, true);
            intent.putExtra(EXTRA_DATA_BATCH, convertToFloatArray(spo2Batch));
        }
 
        // SDNN
        if (sdnn != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
            intent.putExtra(EXTRA_DATA_SDNN, sdnn);
        }
 
        // RR Intervals
        if (extraDataRR != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
            intent.putExtra(EXTRA_DATA_RR, convertToFloatArray(extraDataRR));
        }
 
        if (extraDataBatch != null) {
            if (isDeviceDefault()) {
                for (int i = 0; i < extraDataBatch.size(); i++) {
                    extraDataBatch.set(i, 0.0f);
                }
            }
            intent.putExtra(EXTRA_DATA_BATCH, convertToFloatArray(extraDataBatch));
        }
 
        if (extraDataTimestamp != null) {
            intent.putExtra(EXTRA_DATA_TIMESTAMP, extraDataTimestamp);
        }
        if (extraDataFramerate != null) {
            intent.putExtra(EXTRA_DATA_FRAMERATE, extraDataFramerate);
        }
 
        context.sendBroadcast(intent);
    }
 
    /**
     * This is a generic intent to carry data from various sensors.
     * See Sleep As Android documentation for parameters values.
     * <a href="https://docs.sleep.urbandroid.org/devs/wearable_api.html#send-various-body-sensors-data-hr-rr-spo2-sdnn">...</a>
     */
    public void sendExtra(Float hr, Float extraDataRR, Float spo2, Float sdnn, Long sdnnTimestamp) {
 
        if (!isDeviceDefault() || !trackingOngoing) return;
        if (trackingPaused) return;
        Intent intent = new Intent(ACTION_EXTRA_DATA_UPDATE);
 
        // Heart Rate
        if (hr != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
            intent.putExtra(EXTRA_DATA_HR, hr);
        }
 
        // SpO2
        if (spo2 != null && (hasFeature(SleepAsAndroidFeature.SPO2) && isFeatureEnabled(SleepAsAndroidFeature.SPO2))) {
            intent.putExtra(EXTRA_DATA_SPO2, spo2);
        }
 
        // SDNN
        if (sdnn != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
            intent.putExtra(EXTRA_DATA_SDNN, sdnn);
        }
 
        if (extraDataRR != null && (hasFeature(SleepAsAndroidFeature.HEART_RATE) && isFeatureEnabled(SleepAsAndroidFeature.HEART_RATE))) {
            intent.putExtra(EXTRA_DATA_RR, extraDataRR);
        }
 
        if (sdnnTimestamp != null) {
            intent.putExtra(EXTRA_DATA_TIMESTAMP, sdnnTimestamp);
        }
        broadcastToSleepAsAndroid(intent);
    }
 
    /**
     * Adds 5 minutes of tracking pause time
     */
    public void sendPauseTracking() {
        if (!isDeviceDefault() || !trackingOngoing) return;
        LOG.debug("Sending pause");
        Intent intent = new Intent(ACTION_PAUSE_FROM_WATCH);
        broadcastToSleepAsAndroid(intent);
    }
 
    /**
     * Resume tracking
     */
    public void sendResumeTracking() {
        if (!isDeviceDefault() || !trackingOngoing) return;
        LOG.debug("Sending resume");
        Intent intent = new Intent(ACTION_RESUME_FROM_WATCH);
        broadcastToSleepAsAndroid(intent);
    }
 
    /**
     * Snooze the current alarm
     */
    public void sendSnoozeAlarm() {
        if (!isDeviceDefault()) return;
        if (!hasFeature(SleepAsAndroidFeature.ALARMS) || !isFeatureEnabled(SleepAsAndroidFeature.ALARMS)) return;
        LOG.debug("Sending snooze");
        Intent intent = new Intent(ACTION_SNOOZE_FROM_WATCH);
        broadcastToSleepAsAndroid(intent);
    }
 
    /**
     * Dismiss the current alarm
     */
    public void sendDismissAlarm() {
        if (!isDeviceDefault()) return;
        if (!hasFeature(SleepAsAndroidFeature.ALARMS) || !isFeatureEnabled(SleepAsAndroidFeature.ALARMS)) return;
        LOG.debug("Sending dismiss");
        Intent intent = new Intent(ACTION_DISMISS_FROM_WATCH);
        broadcastToSleepAsAndroid(intent);
    }
 
    private void broadcastToSleepAsAndroid(Intent intent) {
        if (!isDeviceDefault()) return;
        intent.setPackage(PACKAGE_SLEEP_AS_ANDROID);
        GBApplication.getContext().sendBroadcast(intent);
    }
 
    /**
     * Check if the device is set as default provider for Sleep As Android
     *
     * @return true if the device is set as default
     */
    public boolean isDeviceDefault() {
        if (device == null || !device.isInitialized()) return false;
        if (isSleepAsAndroidEnabled()) {
            return device.getAddress().equals(GBApplication.getPrefs().getString("sleepasandroid_device", ""));
        }
        return false;
    }
 
    public boolean isSleepAsAndroidEnabled() {
        return GBApplication.getPrefs().getBoolean("pref_key_sleepasandroid_enable", false);
    }
 
    /**
     * Validate if the device is allowed to receive a specific action
     * @param action the action send my the broadcast receiver
     */
    public void validateAction(String action) {
        if (isDeviceDefault()) {
            switch (action) {
                case SleepAsAndroidAction.HINT:
                case SleepAsAndroidAction.SHOW_NOTIFICATION:
                    if (!hasFeature(SleepAsAndroidFeature.NOTIFICATIONS) || !isFeatureEnabled(SleepAsAndroidFeature.NOTIFICATIONS)) {
                        throw new UnsupportedOperationException("Action not valid");
                    }
                    break;
                case SleepAsAndroidAction.START_ALARM:
                case SleepAsAndroidAction.STOP_ALARM:
                case SleepAsAndroidAction.UPDATE_ALARM:
                    if (!hasFeature(SleepAsAndroidFeature.ALARMS) || !isFeatureEnabled(SleepAsAndroidFeature.ALARMS)) {
                        throw new UnsupportedOperationException("Action not valid");
                    }
                    break;
                case SleepAsAndroidAction.START_TRACKING:
                case SleepAsAndroidAction.STOP_TRACKING:
                case SleepAsAndroidAction.SET_BATCH_SIZE:
                    if (!hasFeature(SleepAsAndroidFeature.ACCELEROMETER) || !isFeatureEnabled(SleepAsAndroidFeature.ACCELEROMETER)) {
                        throw new UnsupportedOperationException("Action not valid");
                    }
                    break;
                default:
                    break;
            }
        } else {
            throw new UnsupportedOperationException("Action not valid");
        }
    }
 
    /**
     * Convert an ArrayList to a float array
     *
     * @param list the ArrayList
     * @return the float array
     */
    private float[] convertToFloatArray(ArrayList<Float> list) {
        float[] result = new float[list.size()];
        int i = 0;
        for (float f : list) {
            result[i++] = f;
        }
        return result;
    }
 
    /**
     * Get configured alarm slot
     *
     * @return the alarm slot to be used for setting alarms on the watch
     */
    public static int getAlarmSlot() {
        Prefs prefs = GBApplication.getPrefs();
        String slotString = prefs.getString("sleepasandroid_alarm_slot", "");
        if (!slotString.isEmpty()) {
            return Integer.parseInt(slotString);
        }
        return 0;
    }
}