diff --git a/README.md b/README.md index 5fc9c3b5..7b8a2423 100644 --- a/README.md +++ b/README.md @@ -68,13 +68,13 @@ Detailed documentation [Full Documentation](polar-sdk-android/docs/html/). ## Installation Compiled sdk and dependencies can be found from [polar-sdk-android](polar-sdk-android/libs/) -1. In `build.gradle` make sure the __minSdkVersion__ is set to __19__ or higher. +1. In `build.gradle` make sure the __minSdkVersion__ is set to __21__ or higher. ``` android { ... defaultConfig { ... - minSdkVersion 19 + minSdkVersion 21 } } ``` diff --git a/demos/Android-Demos/PolarSDK-Exercise-Demo/app/build.gradle b/demos/Android-Demos/PolarSDK-Exercise-Demo/app/build.gradle index b8f91fec..80fdd699 100644 --- a/demos/Android-Demos/PolarSDK-Exercise-Demo/app/build.gradle +++ b/demos/Android-Demos/PolarSDK-Exercise-Demo/app/build.gradle @@ -4,7 +4,7 @@ android { compileSdkVersion 28 defaultConfig { applicationId "com.polar.polarsdkexercisedemo" - minSdkVersion 18 + minSdkVersion 21 targetSdkVersion 28 versionCode 1 versionName "1.0" diff --git a/examples/example-android/androidBleSdkTestApp/app/build.gradle b/examples/example-android/androidBleSdkTestApp/app/build.gradle index 66658ce5..935f8154 100644 --- a/examples/example-android/androidBleSdkTestApp/app/build.gradle +++ b/examples/example-android/androidBleSdkTestApp/app/build.gradle @@ -4,7 +4,7 @@ android { compileSdkVersion 27 defaultConfig { applicationId "polar.com.androidblesdk" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 27 versionCode 1 versionName "1.0" diff --git a/polar-sdk-android/docs/html/BDBleApiImpl_8java_source.html b/polar-sdk-android/docs/html/BDBleApiImpl_8java_source.html index 0fdfd9b7..da818a80 100644 --- a/polar-sdk-android/docs/html/BDBleApiImpl_8java_source.html +++ b/polar-sdk-android/docs/html/BDBleApiImpl_8java_source.html @@ -66,8 +66,8 @@
BDBleApiImpl.java
-Go to the documentation of this file.
1 // Copyright © 2019 Polar Electro Oy. All rights reserved.
2 package polar.com.sdk.impl;
3 
4 import android.annotation.SuppressLint;
5 import android.bluetooth.le.ScanFilter;
6 import android.content.Context;
7 import android.os.Build;
8 import android.os.ParcelUuid;
9 import android.support.annotation.Nullable;
10 import android.util.Log;
11 import android.util.Pair;
12 
13 import com.androidcommunications.polar.api.ble.BleDeviceListener;
14 import com.androidcommunications.polar.api.ble.BleLogger;
15 import com.androidcommunications.polar.api.ble.exceptions.BleDisconnected;
16 import com.androidcommunications.polar.api.ble.model.BleDeviceSession;
17 import com.androidcommunications.polar.api.ble.model.advertisement.BleAdvertisementContent;
18 import com.androidcommunications.polar.api.ble.model.advertisement.BlePolarHrAdvertisement;
19 import com.androidcommunications.polar.api.ble.model.gatt.BleGattBase;
20 import com.androidcommunications.polar.api.ble.model.gatt.client.BleBattClient;
21 import com.androidcommunications.polar.api.ble.model.gatt.client.BleDisClient;
22 import com.androidcommunications.polar.api.ble.model.gatt.client.BleHrClient;
23 import com.androidcommunications.polar.api.ble.model.gatt.client.BlePMDClient;
24 import com.androidcommunications.polar.api.ble.model.gatt.client.psftp.BlePsFtpClient;
25 import com.androidcommunications.polar.api.ble.model.gatt.client.psftp.BlePsFtpUtils;
26 import com.androidcommunications.polar.common.ble.BleUtils;
27 import com.androidcommunications.polar.enpoints.ble.bluedroid.host.BDDeviceListenerImpl;
28 
29 import org.reactivestreams.Publisher;
30 
31 import java.io.ByteArrayOutputStream;
32 import java.text.SimpleDateFormat;
33 import java.util.ArrayList;
34 import java.util.Calendar;
35 import java.util.Collections;
36 import java.util.Comparator;
37 import java.util.Date;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Locale;
42 import java.util.Map;
43 import java.util.Set;
44 import java.util.UUID;
45 import java.util.concurrent.TimeUnit;
46 import java.util.concurrent.atomic.AtomicInteger;
47 
48 import fi.polar.remote.representation.protobuf.ExerciseSamples;
49 import fi.polar.remote.representation.protobuf.Types;
50 import io.reactivex.Completable;
51 import io.reactivex.CompletableEmitter;
52 import io.reactivex.CompletableOnSubscribe;
53 import io.reactivex.CompletableSource;
54 import io.reactivex.Flowable;
55 import io.reactivex.Scheduler;
56 import io.reactivex.Single;
57 import io.reactivex.SingleSource;
58 import io.reactivex.android.schedulers.AndroidSchedulers;
59 import io.reactivex.disposables.Disposable;
60 import io.reactivex.functions.Action;
61 import io.reactivex.functions.BiFunction;
62 import io.reactivex.functions.Consumer;
63 import io.reactivex.functions.Function;
64 import io.reactivex.functions.Predicate;
65 import io.reactivex.schedulers.Timed;
66 import polar.com.sdk.api.PolarBleApi;
86 import protocol.PftpNotification;
87 import protocol.PftpRequest;
88 import protocol.PftpResponse;
89 
93 public class BDBleApiImpl extends PolarBleApi {
94  protected final static String TAG = BDBleApiImpl.class.getSimpleName();
95  protected BleDeviceListener listener;
96  protected Map<String,Disposable> connectSubscriptions = new HashMap<>();
97  protected Scheduler scheduler;
100  protected static final int ANDROID_VERSION_O = 26;
101  BleDeviceListener.BleSearchPreFilter filter = new BleDeviceListener.BleSearchPreFilter() {
102  @Override
103  public boolean process(BleAdvertisementContent content) {
104  return content.getPolarDeviceId().length() != 0 && !content.getPolarDeviceType().equals("mobile");
105  }
106  };
107 
108  @SuppressLint({"NewApi", "CheckResult"})
109  public BDBleApiImpl(final Context context, int features) {
110  super(features);
111  Set<Class<? extends BleGattBase>> clients = new HashSet<>();
112  if((this.features & PolarBleApi.FEATURE_HR)!=0){
113  clients.add(BleHrClient.class);
114  }
115  if((this.features & PolarBleApi.FEATURE_DEVICE_INFO)!=0){
116  clients.add(BleDisClient.class);
117  }
118  if((this.features & PolarBleApi.FEATURE_BATTERY_INFO)!=0){
119  clients.add(BleBattClient.class);
120  }
121  if((this.features & PolarBleApi.FEATURE_POLAR_SENSOR_STREAMING)!=0){
122  clients.add(BlePMDClient.class);
123  }
124  if((this.features & PolarBleApi.FEATURE_POLAR_FILE_TRANSFER)!=0){
125  clients.add(BlePsFtpClient.class);
126  }
127  listener = new BDDeviceListenerImpl(context, clients);
128  listener.setScanPreFilter(filter);
129  scheduler = AndroidSchedulers.from(context.getMainLooper());
130  listener.monitorDeviceSessionState(null).observeOn(scheduler).subscribe(
131  new Consumer<Pair<BleDeviceSession, BleDeviceSession.DeviceSessionState>>() {
132  @Override
133  public void accept(Pair<BleDeviceSession, BleDeviceSession.DeviceSessionState> pair) throws Exception {
134  PolarDeviceInfo info = new PolarDeviceInfo(
135  pair.first.getPolarDeviceId().length() != 0 ?
136  pair.first.getPolarDeviceId() : pair.first.getAddress(),
137  pair.first.getAddress(),
138  pair.first.getRssi(),pair.first.getName(),true);
139  switch (pair.second){
140  case SESSION_OPEN:
141  if(callback!=null){
143  }
144  setupDevice(pair.first);
145  break;
146  case SESSION_CLOSED:
147  if( callback != null ) {
148  if (pair.first.getPreviousState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN ||
149  pair.first.getPreviousState() == BleDeviceSession.DeviceSessionState.SESSION_CLOSING){
151  }
152  }
153  break;
154  case SESSION_OPENING:
155  if(callback != null){
157  }
158  break;
159  }
160  }
161  },
162  new Consumer<Throwable>() {
163  @Override
164  public void accept(Throwable throwable) throws Exception {
165  logError(throwable.getMessage());
166  }
167  },
168  new Action() {
169  @Override
170  public void run() throws Exception {
171 
172  }
173  }
174  );
175  listener.monitorBleState().observeOn(scheduler).subscribe(
176  new Consumer<Boolean>() {
177  @Override
178  public void accept(Boolean aBoolean) throws Exception {
179  if(callback != null){
180  callback.blePowerStateChanged(aBoolean);
181  }
182  }
183  },
184  new Consumer<Throwable>() {
185  @Override
186  public void accept(Throwable throwable) throws Exception {
187  logError(throwable.getMessage());
188  }
189  },
190  new Action() {
191  @Override
192  public void run() throws Exception {
193 
194  }
195  }
196  );
197  BleLogger.setLoggerInterface(new BleLogger.BleLoggerInterface() {
198  @Override
199  public void d(String tag, String msg) {
200  log(tag+"/"+msg);
201  }
202 
203  @Override
204  public void e(String tag, String msg) {
205  logError(tag+"/"+msg);
206  }
207 
208  @Override
209  public void w(String tag, String msg) {
210  }
211 
212  @Override
213  public void i(String tag, String msg) {
214  }
215  });
216  }
217 
218  @SuppressLint("NewApi")
219  protected void enableAndroidScanFilter() {
220  if (Build.VERSION.SDK_INT >= ANDROID_VERSION_O) {
221  List<ScanFilter> filter = new ArrayList<>();
222  filter.add(new ScanFilter.Builder().setServiceUuid(
223  ParcelUuid.fromString(BleHrClient.HR_SERVICE.toString())).build());
224  filter.add(new ScanFilter.Builder().setServiceUuid(
225  ParcelUuid.fromString(BlePsFtpUtils.RFC77_PFTP_SERVICE.toString())).build());
226  listener.setScanFilters(filter);
227  }
228  }
229 
230  @Override
231  public void shutDown() {
232  listener.shutDown();
233  }
234 
235  @Override
236  public void cleanup() {
237  listener.removeAllSessions();
238  }
239 
240  @Override
241  public void setPolarFilter(boolean enable) {
242  if(!enable) {
243  listener.setScanPreFilter(null);
244  } else {
245  listener.setScanPreFilter(filter);
246  }
247  }
248 
249  @Override
250  public boolean isFeatureReady(final String deviceId, int feature) {
251  try {
252  switch (feature) {
254  return sessionPsFtpClientReady(deviceId) != null;
256  return sessionPmdClientReady(deviceId) != null;
257  }
258  } catch (Throwable ignored) {
259  }
260  return false;
261  }
262 
263  @Override
265  this.callback = callback;
267  }
268 
269  @Override
270  public void setApiLogger(@Nullable PolarBleApiLogger logger) {
271  this.logger = logger;
272  }
273 
274  @Override
275  public void setAutomaticReconnection(boolean disable) {
276  listener.setAutomaticReconnection(disable);
277  }
278 
279  @Override
280  public Completable setLocalTime(String identifier, Calendar cal) {
281  try {
282  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
283  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
284  PftpRequest.PbPFtpSetLocalTimeParams.Builder builder = PftpRequest.PbPFtpSetLocalTimeParams.newBuilder();
285  Types.PbDate date = Types.PbDate.newBuilder()
286  .setYear(cal.get(Calendar.YEAR))
287  .setMonth(cal.get(Calendar.MONTH) + 1)
288  .setDay(cal.get(Calendar.DAY_OF_MONTH)).build();
289  Types.PbTime time = Types.PbTime.newBuilder()
290  .setHour(cal.get(Calendar.HOUR_OF_DAY))
291  .setMinute(cal.get(Calendar.MINUTE))
292  .setSeconds(cal.get(Calendar.SECOND))
293  .setMillis(cal.get(Calendar.MILLISECOND)).build();
294  builder.setDate(date).setTime(time).setTzOffset((int) TimeUnit.MINUTES.convert(cal.get(Calendar.ZONE_OFFSET), TimeUnit.MILLISECONDS));
295  return client.query(PftpRequest.PbPFtpQuery.SET_LOCAL_TIME_VALUE,builder.build().toByteArray()).toObservable().ignoreElements();
296  } catch (Throwable error){
297  return Completable.error(error);
298  }
299  }
300 
301  @Override
302  public Single<PolarSensorSetting> requestAccSettings(String identifier) {
303  return querySettings(identifier,BlePMDClient.PmdMeasurementType.ACC);
304  }
305 
306  @Override
307  public Single<PolarSensorSetting> requestEcgSettings(String identifier) {
308  return querySettings(identifier,BlePMDClient.PmdMeasurementType.ECG);
309  }
310 
311  @Override
312  public Single<PolarSensorSetting> requestPpgSettings(String identifier) {
313  return querySettings(identifier,BlePMDClient.PmdMeasurementType.PPG);
314  }
315 
316  @Override
317  public Single<PolarSensorSetting> requestBiozSettings(final String identifier){
318  return querySettings(identifier,BlePMDClient.PmdMeasurementType.BIOZ);
319  }
320 
321  protected Single<PolarSensorSetting> querySettings(final String identifier, final BlePMDClient.PmdMeasurementType type) {
322  try {
323  final BleDeviceSession session = sessionPmdClientReady(identifier);
324  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
325  return client.querySettings(type).map(new Function<BlePMDClient.PmdSetting, PolarSensorSetting>() {
326  @Override
327  public PolarSensorSetting apply(BlePMDClient.PmdSetting setting) throws Exception {
328  return new PolarSensorSetting(setting.settings, type);
329  }
330  });
331  } catch (Throwable e){
332  return Single.error(e);
333  }
334  }
335 
336  @Override
337  public void backgroundEntered() {
339  }
340 
341  @Override
342  public void foregroundEntered() {
343  listener.setScanFilters(null);
344  }
345 
346  @Override
347  public Completable autoConnectToDevice(final int rssiLimit, final String service, final int timeout, final TimeUnit unit, final String polarDeviceType) {
348  final long[] start = {0};
349  return Completable.create(new CompletableOnSubscribe() {
350  @Override
351  public void subscribe(CompletableEmitter emitter) throws Exception {
352  if( service == null || service.matches("([0-9a-fA-F]{4})") ) {
353  emitter.onComplete();
354  } else {
355  emitter.tryOnError(new PolarInvalidArgument("Invalid service string format"));
356  }
357  }
358  }).andThen(listener.search(false).filter(new Predicate<BleDeviceSession>() {
359  @Override
360  public boolean test(BleDeviceSession bleDeviceSession) throws Exception {
361  if( bleDeviceSession.getMedianRssi() >= rssiLimit &&
362  bleDeviceSession.isConnectableAdvertisement() &&
363  (polarDeviceType == null || polarDeviceType.equals(bleDeviceSession.getPolarDeviceType())) &&
364  (service == null || bleDeviceSession.getAdvertisementContent().containsService(service)) ) {
365  if(start[0] == 0){
366  start[0] = System.currentTimeMillis();
367  }
368  return true;
369  }
370  return false;
371  }
372  }).timestamp().takeUntil(new Predicate<Timed<BleDeviceSession>>() {
373  @Override
374  public boolean test(Timed<BleDeviceSession> bleDeviceSessionTimed) throws Exception {
375  long diff = bleDeviceSessionTimed.time(TimeUnit.MILLISECONDS) - start[0];
376  return (diff >= unit.toMillis(timeout));
377  }
378  }).reduce(new HashSet<BleDeviceSession>(), new BiFunction<Set<BleDeviceSession>, Timed<BleDeviceSession>, Set<BleDeviceSession>>() {
379  @Override
380  public Set<BleDeviceSession> apply(Set<BleDeviceSession> objects, Timed<BleDeviceSession> bleDeviceSessionTimed) throws Exception {
381  objects.add(bleDeviceSessionTimed.value());
382  return objects;
383  }
384  }).doOnSuccess(new Consumer<Set<BleDeviceSession>>() {
385  @Override
386  public void accept(Set<BleDeviceSession> set) throws Exception {
387  List<BleDeviceSession> list = new ArrayList<>(set);
388  Collections.sort(list, new Comparator<BleDeviceSession>() {
389  @Override
390  public int compare(BleDeviceSession o1, BleDeviceSession o2) {
391  return o1.getRssi() > o2.getRssi() ? -1 : 1;
392  }
393  });
394  listener.openSessionDirect(list.get(0));
395  log("auto connect search complete");
396  }
397  }).toObservable().ignoreElements());
398  }
399 
400  @Override
401  public Completable autoConnectToDevice(final int rssiLimit, final String service, final String polarDeviceType) {
402  return autoConnectToDevice(rssiLimit, service, 2, TimeUnit.SECONDS, polarDeviceType);
403  }
404 
405  @Override
406  public void connectToDevice(final String identifier) throws PolarInvalidArgument {
407  BleDeviceSession session = fetchSession(identifier);
408  if( session == null || session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_CLOSED ){
409  if( connectSubscriptions.containsKey(identifier) ){
410  connectSubscriptions.get(identifier).dispose();
411  connectSubscriptions.remove(identifier);
412  }
413  if( session != null ){
414  listener.openSessionDirect(session);
415  } else {
416  connectSubscriptions.put(identifier, listener.search(false).filter(new Predicate<BleDeviceSession>() {
417  @Override
418  public boolean test(BleDeviceSession bleDeviceSession) throws Exception {
419  return identifier.contains(":") ?
420  bleDeviceSession.getAddress().equals(identifier) :
421  bleDeviceSession.getPolarDeviceId().equals(identifier);
422  }
423  }).take(1).observeOn(scheduler).subscribe(
424  new Consumer<BleDeviceSession>() {
425  @Override
426  public void accept(BleDeviceSession bleDeviceSession) throws Exception {
427  listener.openSessionDirect(bleDeviceSession);
428  }
429  },
430  new Consumer<Throwable>() {
431  @Override
432  public void accept(Throwable throwable) throws Exception {
433  logError(throwable.getMessage());
434  }
435  },
436  new Action() {
437  @Override
438  public void run() throws Exception {
439  log("connect search complete");
440  }
441  }
442  ));
443  }
444  }
445  }
446 
447  @Override
448  public void disconnectFromDevice(String identifier) throws PolarInvalidArgument {
449  BleDeviceSession session = fetchSession(identifier);
450  if( session != null ){
451  if( session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN ||
452  session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPENING ||
453  session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK ) {
454  listener.closeSessionDirect(session);
455  }
456  }
457  if (connectSubscriptions.containsKey(identifier)){
458  connectSubscriptions.get(identifier).dispose();
459  connectSubscriptions.remove(identifier);
460  }
461  }
462 
463  @Override
464  public Completable startRecording(String identifier, String exerciseId, RecordingInterval interval, SampleType type) {
465  try {
466  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
467  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
468  if(session.getPolarDeviceType().equals("H10")) {
469  Types.PbSampleType t = type == SampleType.HR ?
470  Types.PbSampleType.SAMPLE_TYPE_HEART_RATE :
471  Types.PbSampleType.SAMPLE_TYPE_RR_INTERVAL;
472  Types.PbDuration duration = Types.PbDuration.newBuilder().setSeconds(interval.getValue()).build();
473  PftpRequest.PbPFtpRequestStartRecordingParams params = PftpRequest.PbPFtpRequestStartRecordingParams.newBuilder().
474  setSampleDataIdentifier(exerciseId).setSampleType(t).setRecordingInterval(duration).build();
475  return client.query(PftpRequest.PbPFtpQuery.REQUEST_START_RECORDING_VALUE, params.toByteArray()).toObservable().ignoreElements().onErrorResumeNext(new Function<Throwable, CompletableSource>() {
476  @Override
477  public CompletableSource apply(Throwable throwable) throws Exception {
478  return Completable.error(throwable);
479  }
480  });
481  }
482  return Completable.error(new PolarOperationNotSupported());
483  } catch (Throwable error){
484  return Completable.error(error);
485  }
486  }
487 
488  @Override
489  public Completable stopRecording(String identifier) {
490  try {
491  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
492  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
493  if(session.getPolarDeviceType().equals("H10")) {
494  return client.query(PftpRequest.PbPFtpQuery.REQUEST_STOP_RECORDING_VALUE, null).toObservable().ignoreElements().onErrorResumeNext(new Function<Throwable, CompletableSource>() {
495  @Override
496  public CompletableSource apply(Throwable throwable) throws Exception {
497  return Completable.error(handleError(throwable));
498  }
499  });
500  }
501  return Completable.error(new PolarOperationNotSupported());
502  } catch (Throwable error){
503  return Completable.error(error);
504  }
505  }
506 
507  @Override
508  public Single<Pair<Boolean,String>> requestRecordingStatus(String identifier) {
509  try {
510  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
511  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
512  if(session.getPolarDeviceType().equals("H10")) {
513  return client.query(PftpRequest.PbPFtpQuery.REQUEST_RECORDING_STATUS_VALUE, null).map(new Function<ByteArrayOutputStream, Pair<Boolean,String>>() {
514  @Override
515  public Pair<Boolean,String> apply(ByteArrayOutputStream byteArrayOutputStream) throws Exception {
516  PftpResponse.PbRequestRecordingStatusResult result = PftpResponse.PbRequestRecordingStatusResult.parseFrom(byteArrayOutputStream.toByteArray());
517  return new Pair<>(result.getRecordingOn(),result.hasSampleDataIdentifier() ? result.getSampleDataIdentifier() : "");
518  }
519  }).onErrorResumeNext(new Function<Throwable, SingleSource<? extends Pair<Boolean, String>>>() {
520  @Override
521  public SingleSource<? extends Pair<Boolean, String>> apply(Throwable throwable) throws Exception {
522  return Single.error(handleError(throwable));
523  }
524  });
525  }
526  return Single.error(new PolarOperationNotSupported());
527  } catch (Throwable error){
528  return Single.error(error);
529  }
530  }
531 
532  @Override
533  public Flowable<PolarExerciseEntry> listExercises(String identifier) {
534  try{
535  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
536  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
537  switch (session.getPolarDeviceType()) {
538  case "OH1":
539  return fetchRecursively(client, "/U/0/", new FetchRecursiveCondition() {
540  @Override
541  public boolean include(String entry) {
542  return entry.matches("^([0-9]{8})(\\/)") ||
543  entry.matches("^([0-9]{6})(\\/)") ||
544  entry.equals("E/") ||
545  entry.equals("SAMPLES.BPB") ||
546  entry.equals("00/");
547  }
548  }).map(new Function<String, PolarExerciseEntry>() {
549  @Override
550  public PolarExerciseEntry apply(String p) throws Exception {
551  String components[] = p.split("/");
552  SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd HHmmss", Locale.getDefault());
553  Date date = format.parse(components[3] + " " + components[5]);
554  return new PolarExerciseEntry(p, date, components[3] + components[5]);
555  }
556  }).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarExerciseEntry>>() {
557  @Override
558  public Publisher<? extends PolarExerciseEntry> apply(Throwable throwable) throws Exception {
559  return Flowable.error(handleError(throwable));
560  }
561  });
562  case "H10":
563  return fetchRecursively(client, "/", new FetchRecursiveCondition() {
564  @Override
565  public boolean include(String entry) {
566  return entry.endsWith("/") || entry.equals("SAMPLES.BPB");
567  }
568  }).map(new Function<String, PolarExerciseEntry>() {
569  @Override
570  public PolarExerciseEntry apply(String p) throws Exception {
571  String components[] = p.split("/");
572  return new PolarExerciseEntry(p, new Date(), components[1]);
573  }
574  }).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarExerciseEntry>>() {
575  @Override
576  public Publisher<? extends PolarExerciseEntry> apply(Throwable throwable) throws Exception {
577  return Flowable.error(handleError(throwable));
578  }
579  });
580  default:
581  return Flowable.error(new PolarOperationNotSupported());
582  }
583  } catch (Throwable error){
584  return Flowable.error(error);
585  }
586  }
587 
588  @Override
589  public Single<PolarExerciseData> fetchExercise(String identifier, PolarExerciseEntry entry) {
590  try{
591  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
592  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
593  protocol.PftpRequest.PbPFtpOperation.Builder builder = protocol.PftpRequest.PbPFtpOperation.newBuilder();
594  builder.setCommand(PftpRequest.PbPFtpOperation.Command.GET);
595  builder.setPath(entry.path);
596  if(session.getPolarDeviceType().equals("OH1") || session.getPolarDeviceType().equals("H10")) {
597  return client.request(builder.build().toByteArray()).map(new Function<ByteArrayOutputStream, PolarExerciseData>() {
598  @Override
599  public PolarExerciseData apply(ByteArrayOutputStream byteArrayOutputStream) throws Exception {
600  ExerciseSamples.PbExerciseSamples samples = ExerciseSamples.PbExerciseSamples.parseFrom(byteArrayOutputStream.toByteArray());
601  if(samples.hasRrSamples()){
602  return new PolarExerciseData(samples.getRecordingInterval().getSeconds(), samples.getRrSamples().getRrIntervalsList());
603  } else {
604  return new PolarExerciseData(samples.getRecordingInterval().getSeconds(), samples.getHeartRateSamplesList());
605  }
606  }
607  }).onErrorResumeNext(new Function<Throwable, SingleSource<? extends PolarExerciseData>>() {
608  @Override
609  public SingleSource<? extends PolarExerciseData> apply(Throwable throwable) throws Exception {
610  return Single.error(handleError(throwable));
611  }
612  });
613  }
614  return Single.error(new PolarOperationNotSupported());
615  } catch (Throwable error){
616  return Single.error(error);
617  }
618  }
619 
620  @Override
621  public Completable removeExercise(String identifier, PolarExerciseEntry entry) {
622  try{
623  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
624  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
625  if(session.getPolarDeviceType().equals("OH1")){
626  protocol.PftpRequest.PbPFtpOperation.Builder builder = protocol.PftpRequest.PbPFtpOperation.newBuilder();
627  builder.setCommand(PftpRequest.PbPFtpOperation.Command.GET);
628  final String components[] = entry.path.split("/");
629  final String exerciseParent = "/U/0/" + components[3] + "/E/";
630  builder.setPath(exerciseParent);
631  return client.request(builder.build().toByteArray()).flatMap(new Function<ByteArrayOutputStream, SingleSource<?>>() {
632  @Override
633  public SingleSource<?> apply(ByteArrayOutputStream byteArrayOutputStream) throws Exception {
634  PftpResponse.PbPFtpDirectory directory = PftpResponse.PbPFtpDirectory.parseFrom(byteArrayOutputStream.toByteArray());
635  protocol.PftpRequest.PbPFtpOperation.Builder removeBuilder = protocol.PftpRequest.PbPFtpOperation.newBuilder();
636  removeBuilder.setCommand(PftpRequest.PbPFtpOperation.Command.REMOVE);
637  if( directory.getEntriesCount() <= 1 ){
638  // remove entire directory
639  removeBuilder.setPath("/U/0/" + components[3] + "/");
640  } else {
641  // remove only exercise
642  removeBuilder.setPath("/U/0/" + components[3] + "/E/" + components[5] + "/");
643  }
644  return client.request(removeBuilder.build().toByteArray());
645  }
646  }).toObservable().ignoreElements().onErrorResumeNext(new Function<Throwable, CompletableSource>() {
647  @Override
648  public CompletableSource apply(Throwable throwable) throws Exception {
649  return Completable.error(handleError(throwable));
650  }
651  });
652  } else if(session.getPolarDeviceType().equals("H10")){
653  protocol.PftpRequest.PbPFtpOperation.Builder builder = protocol.PftpRequest.PbPFtpOperation.newBuilder();
654  builder.setCommand(PftpRequest.PbPFtpOperation.Command.REMOVE);
655  builder.setPath(entry.path);
656  return client.request(builder.build().toByteArray()).toObservable().ignoreElements().onErrorResumeNext(new Function<Throwable, CompletableSource>() {
657  @Override
658  public CompletableSource apply(Throwable throwable) throws Exception {
659  return Completable.error(handleError(throwable));
660  }
661  });
662  }
663  return Completable.error(new PolarOperationNotSupported());
664  } catch (Throwable error){
665  return Completable.error(error);
666  }
667  }
668 
669  @Override
670  public Flowable<PolarDeviceInfo> searchForDevice() {
671  return listener.search(false).distinct().map(new Function<BleDeviceSession, PolarDeviceInfo>() {
672  @Override
673  public PolarDeviceInfo apply(BleDeviceSession bleDeviceSession) throws Exception {
674  return new PolarDeviceInfo(bleDeviceSession.getPolarDeviceId(),
675  bleDeviceSession.getAddress(),
676  bleDeviceSession.getRssi(),
677  bleDeviceSession.getName(),
678  bleDeviceSession.isConnectableAdvertisement());
679  }
680  });
681  }
682 
683  @Override
684  public Flowable<PolarHrBroadcastData> startListenForPolarHrBroadcasts(final Set<String> deviceIds) {
685  // set filter to null, NOTE this disables reconnection in background
686  return listener.search(false).filter(new Predicate<BleDeviceSession>() {
687  @Override
688  public boolean test(BleDeviceSession bleDeviceSession) throws Exception {
689  return (deviceIds == null || deviceIds.contains(bleDeviceSession.getPolarDeviceId())) &&
690  bleDeviceSession.getAdvertisementContent().getPolarHrAdvertisement().isPresent();
691  }
692  }).map(new Function<BleDeviceSession, PolarHrBroadcastData>() {
693  @Override
694  public PolarHrBroadcastData apply(BleDeviceSession bleDeviceSession) throws Exception {
695  BlePolarHrAdvertisement advertisement = bleDeviceSession.getBlePolarHrAdvertisement();
696  return new PolarHrBroadcastData( new PolarDeviceInfo(bleDeviceSession.getPolarDeviceId(),
697  bleDeviceSession.getAddress(),
698  bleDeviceSession.getRssi(),
699  bleDeviceSession.getName(),
700  bleDeviceSession.isConnectableAdvertisement()),
701  advertisement.getHrForDisplay(),
702  advertisement.getBatteryStatus() != 0);
703  }
704  });
705  }
706 
707  @Override
708  public Flowable<PolarEcgData> startEcgStreaming(String identifier,
709  PolarSensorSetting setting) {
710  try {
711  final BleDeviceSession session = sessionPmdClientReady(identifier);
712  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
713  return client.startMeasurement(BlePMDClient.PmdMeasurementType.ECG, setting.map2PmdSettings()).andThen(
714  client.monitorEcgNotifications(true).map(new Function<BlePMDClient.EcgData, PolarEcgData>() {
715  @Override
716  public PolarEcgData apply(BlePMDClient.EcgData ecgData) throws Exception {
717  List<Integer> samples = new ArrayList<>();
718  for( BlePMDClient.EcgData.EcgSample s : ecgData.ecgSamples ){
719  samples.add(s.microVolts);
720  }
721  return new PolarEcgData(samples,ecgData.timeStamp);
722  }
723  }).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarEcgData>>() {
724  @Override
725  public Publisher<? extends PolarEcgData> apply(Throwable throwable) throws Exception {
726  return Flowable.error(handleError(throwable));
727  }
728  }).doFinally(new Action() {
729  @Override
730  public void run() throws Exception {
731  stopPmdStreaming(session,client, BlePMDClient.PmdMeasurementType.ECG);
732  }
733  }));
734  } catch (Throwable t){
735  return Flowable.error(t);
736  }
737  }
738 
739  @Override
740  public Flowable<PolarAccelerometerData> startAccStreaming(String identifier,
741  PolarSensorSetting setting) {
742  try {
743  final BleDeviceSession session = sessionPmdClientReady(identifier);
744  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
745  return client.startMeasurement(BlePMDClient.PmdMeasurementType.ACC, setting.map2PmdSettings()).andThen(
746  client.monitorAccNotifications(true).map(new Function<BlePMDClient.AccData, PolarAccelerometerData>() {
747  @Override
748  public PolarAccelerometerData apply(BlePMDClient.AccData accData) throws Exception {
749  List<PolarAccelerometerData.PolarAccelerometerSample> samples = new ArrayList<>();
750  for( BlePMDClient.AccData.AccSample s : accData.accSamples ){
751  samples.add(new PolarAccelerometerData.PolarAccelerometerSample(s.x,s.y,s.z));
752  }
753  return new PolarAccelerometerData(samples,accData.timeStamp);
754  }
755  }).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarAccelerometerData>>() {
756  @Override
757  public Publisher<? extends PolarAccelerometerData> apply(Throwable throwable) throws Exception {
758  return Flowable.error(handleError(throwable));
759  }
760  }).doFinally(new Action() {
761  @Override
762  public void run() throws Exception {
763  stopPmdStreaming(session,client, BlePMDClient.PmdMeasurementType.ACC);
764  }
765  }));
766  } catch (Throwable t){
767  return Flowable.error(t);
768  }
769  }
770 
771  @Override
772  public Flowable<PolarOhrPPGData> startOhrPPGStreaming(String identifier,
773  PolarSensorSetting setting) {
774  try {
775  final BleDeviceSession session = sessionPmdClientReady(identifier);
776  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
777  return client.startMeasurement(BlePMDClient.PmdMeasurementType.PPG, setting.map2PmdSettings()).andThen(
778  client.monitorPpgNotifications(true).map(new Function<BlePMDClient.PpgData, PolarOhrPPGData>() {
779  @Override
780  public PolarOhrPPGData apply(BlePMDClient.PpgData ppgData) throws Exception {
781  List<PolarOhrPPGData.PolarOhrPPGSample> samples = new ArrayList<>();
782  for( BlePMDClient.PpgData.PpgSample s : ppgData.ppgSamples ){
783  samples.add(new PolarOhrPPGData.PolarOhrPPGSample(s.ppg0,s.ppg1,s.ppg2,s.ambient,s.ppgDataSamples,s.ambient1,s.status));
784  }
785  return new PolarOhrPPGData(samples,ppgData.timeStamp);
786  }
787  }).doFinally(new Action() {
788  @Override
789  public void run() throws Exception {
790  stopPmdStreaming(session,client, BlePMDClient.PmdMeasurementType.PPG);
791  }
792  })).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarOhrPPGData>>() {
793  @Override
794  public Publisher<? extends PolarOhrPPGData> apply(Throwable throwable) throws Exception {
795  return Flowable.error(handleError(throwable));
796  }
797  });
798  } catch (Throwable t){
799  return Flowable.error(t);
800  }
801  }
802 
803  @Override
804  public Flowable<PolarOhrPPIData> startOhrPPIStreaming(String identifier) {
805  try {
806  final BleDeviceSession session = sessionPmdClientReady(identifier);
807  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
808  return client.startMeasurement(BlePMDClient.PmdMeasurementType.PPI, new BlePMDClient.PmdSetting(new HashMap<BlePMDClient.PmdSetting.PmdSettingType, Integer>())).andThen(
809  client.monitorPpiNotifications(true).map(new Function<BlePMDClient.PpiData, PolarOhrPPIData>() {
810  @Override
811  public PolarOhrPPIData apply(BlePMDClient.PpiData ppiData) throws Exception {
812  List<PolarOhrPPIData.PolarOhrPPISample> samples = new ArrayList<>();
813  for(BlePMDClient.PpiData.PPSample ppSample : ppiData.ppSamples){
814  samples.add(new PolarOhrPPIData.PolarOhrPPISample(ppSample.ppInMs,
815  ppSample.ppErrorEstimate,
816  ppSample.hr,
817  ppSample.blockerBit != 0,
818  ppSample.skinContactStatus != 0,
819  ppSample.skinContactSupported != 0));
820  }
821  return new PolarOhrPPIData(ppiData.timestamp,samples);
822  }
823  }).doFinally(new Action() {
824  @Override
825  public void run() throws Exception {
826  stopPmdStreaming(session,client, BlePMDClient.PmdMeasurementType.PPI);
827  }
828  })).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarOhrPPIData>>() {
829  @Override
830  public Publisher<? extends PolarOhrPPIData> apply(Throwable throwable) throws Exception {
831  return Flowable.error(handleError(throwable));
832  }
833  });
834  } catch (Throwable t){
835  return Flowable.error(t);
836  }
837  }
838 
839  @Override
840  public Flowable<PolarBiozData> startBiozStreaming(final String identifier, PolarSensorSetting setting){
841  try {
842  final BleDeviceSession session = sessionPmdClientReady(identifier);
843  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
844  return client.startMeasurement(BlePMDClient.PmdMeasurementType.BIOZ, setting.map2PmdSettings()).andThen(
845  client.monitorBiozNotifications(true).map(new Function<BlePMDClient.BiozData, PolarBiozData>() {
846  @Override
847  public PolarBiozData apply(BlePMDClient.BiozData biozData) throws Exception {
848  return new PolarBiozData(biozData.timeStamp,biozData.samples,biozData.status);
849  }
850  }).doFinally(new Action() {
851  @Override
852  public void run() throws Exception {
853  stopPmdStreaming(session,client, BlePMDClient.PmdMeasurementType.PPG);
854  }
855  })).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarBiozData>>() {
856  @Override
857  public Publisher<? extends PolarBiozData> apply(Throwable throwable) throws Exception {
858  return Flowable.error(handleError(throwable));
859  }
860  });
861  } catch (Throwable t){
862  return Flowable.error(t);
863  }
864  }
865 
866  protected BleDeviceSession fetchSession(final String identifier) throws PolarInvalidArgument {
867  if(identifier.matches("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")){
868  return sessionByAddress(identifier);
869  } else if(identifier.matches("([0-9a-fA-F]){6,8}")) {
870  return sessionByDeviceId(identifier);
871  }
872  throw new PolarInvalidArgument();
873  }
874 
875  protected BleDeviceSession sessionByAddress(final String address) throws PolarInvalidArgument {
876  for ( BleDeviceSession session : listener.deviceSessions() ){
877  if( session.getAddress().equals(address) ){
878  return session;
879  }
880  }
881  return null;
882  }
883 
884  protected BleDeviceSession sessionByDeviceId(final String deviceId) throws PolarInvalidArgument {
885  for ( BleDeviceSession session : listener.deviceSessions() ){
886  if( session.getAdvertisementContent().getPolarDeviceId().equals(deviceId) ){
887  return session;
888  }
889  }
890  return null;
891  }
892 
893  protected BleDeviceSession sessionServiceReady(final String identifier, UUID service) throws Throwable {
894  BleDeviceSession session = fetchSession(identifier);
895  if(session != null){
896  if(session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN) {
897  BleGattBase client = session.fetchClient(service);
898  if (client.isServiceDiscovered()) {
899  return session;
900  }
901  throw new PolarServiceNotAvailable();
902  }
903  throw new PolarDeviceDisconnected();
904  }
905  throw new PolarDeviceNotFound();
906  }
907 
908  public BleDeviceSession sessionPmdClientReady(final String identifier) throws Throwable {
909  BleDeviceSession session = sessionServiceReady(identifier, BlePMDClient.PMD_SERVICE);
910  BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
911  final AtomicInteger pair = client.getNotificationAtomicInteger(BlePMDClient.PMD_CP);
912  final AtomicInteger pairData = client.getNotificationAtomicInteger(BlePMDClient.PMD_DATA);
913  if (pair != null && pairData != null &&
914  pair.get() == BleGattBase.ATT_SUCCESS &&
915  pairData.get() == BleGattBase.ATT_SUCCESS) {
916  return session;
917  }
918  throw new PolarNotificationNotEnabled();
919  }
920 
921  protected BleDeviceSession sessionPsFtpClientReady(final String identifier) throws Throwable {
922  BleDeviceSession session = sessionServiceReady(identifier, BlePsFtpUtils.RFC77_PFTP_SERVICE);
923  BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
924  final AtomicInteger pair = client.getNotificationAtomicInteger(BlePsFtpUtils.RFC77_PFTP_MTU_CHARACTERISTIC);
925  if (pair != null && pair.get() == BleGattBase.ATT_SUCCESS ) {
926  return session;
927  }
928  throw new PolarNotificationNotEnabled();
929  }
930 
931  @SuppressLint("CheckResult")
932  protected void stopPmdStreaming(BleDeviceSession session, BlePMDClient client, BlePMDClient.PmdMeasurementType type) {
933  if( session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN ){
934  // stop streaming
935  client.stopMeasurement(type).subscribe(
936  new Action() {
937  @Override
938  public void run() throws Exception {
939 
940  }
941  },
942  new Consumer<Throwable>() {
943  @Override
944  public void accept(Throwable throwable) throws Exception {
945  logError("failed to stop pmd stream: " + throwable.getLocalizedMessage());
946  }
947  }
948  );
949  }
950  }
951 
952  @SuppressLint("CheckResult")
953  protected void setupDevice(final BleDeviceSession session){
954  final String deviceId = session.getPolarDeviceId().length() != 0 ? session.getPolarDeviceId() : session.getAddress();
955  session.monitorServicesDiscovered(true).observeOn(scheduler).toFlowable().flatMapIterable(
956  new Function<List<UUID>, Iterable<UUID>>() {
957  @Override
958  public Iterable<UUID> apply(List<UUID> uuids) throws Exception {
959  return uuids;
960  }
961  }
962  ).flatMap(new Function<UUID, Publisher<?>>() {
963  @Override
964  public Publisher<?> apply(UUID uuid) throws Exception {
965  if(session.fetchClient(uuid) != null) {
966  if (uuid.equals(BleHrClient.HR_SERVICE)) {
967  if (callback != null) {
968  callback.hrFeatureReady(deviceId);
969  }
970  final BleHrClient client = (BleHrClient) session.fetchClient(BleHrClient.HR_SERVICE);
971  client.observeHrNotifications(true).observeOn(scheduler).subscribe(
972  new Consumer<BleHrClient.HrNotificationData>() {
973  @Override
974  public void accept(BleHrClient.HrNotificationData hrNotificationData) throws Exception {
975  if (callback != null) {
977  new PolarHrData(hrNotificationData.hrValue,
978  hrNotificationData.rrs,
979  hrNotificationData.sensorContact,
980  hrNotificationData.sensorContactSupported,
981  hrNotificationData.rrPresent));
982  }
983  }
984  },
985  new Consumer<Throwable>() {
986  @Override
987  public void accept(Throwable throwable) throws Exception {
988  logError(throwable.getMessage());
989  }
990  },
991  new Action() {
992  @Override
993  public void run() throws Exception {
994 
995  }
996  }
997  );
998  } else if (uuid.equals(BleBattClient.BATTERY_SERVICE)) {
999  BleBattClient client = (BleBattClient) session.fetchClient(BleBattClient.BATTERY_SERVICE);
1000  return client.waitBatteryLevelUpdate(true).observeOn(scheduler).doOnSuccess(new Consumer<Integer>() {
1001  @Override
1002  public void accept(Integer integer) throws Exception {
1003  if (callback != null) {
1004  callback.batteryLevelReceived(deviceId, integer);
1005  }
1006  }
1007  }).toFlowable();
1008  } else if (uuid.equals(BlePMDClient.PMD_SERVICE)) {
1009  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
1010  return client.waitNotificationEnabled(BlePMDClient.PMD_CP, true).
1011  concatWith(client.waitNotificationEnabled(BlePMDClient.PMD_DATA, true)).andThen(client.readFeature(true).doOnSuccess(new Consumer<BlePMDClient.PmdFeature>() {
1012  @Override
1013  public void accept(BlePMDClient.PmdFeature pmdFeature) {
1014  if (callback != null) {
1015  if (pmdFeature.ecgSupported) {
1016  callback.ecgFeatureReady(deviceId);
1017  }
1018  if (pmdFeature.accSupported) {
1020  }
1021  if (pmdFeature.ppgSupported) {
1022  callback.ppgFeatureReady(deviceId);
1023  }
1024  if (pmdFeature.ppiSupported) {
1025  callback.ppiFeatureReady(deviceId);
1026  }
1027  if (pmdFeature.bioZSupported) {
1028  callback.biozFeatureReady(deviceId);
1029  }
1030  }
1031  }
1032  })).toFlowable();
1033  } else if (uuid.equals(BleDisClient.DIS_SERVICE)) {
1034  BleDisClient client = (BleDisClient) session.fetchClient(BleDisClient.DIS_SERVICE);
1035  return client.observeDisInfo(true).observeOn(scheduler).doOnNext(new Consumer<Pair<UUID, String>>() {
1036  @Override
1037  public void accept(Pair<UUID, String> pair) {
1038  if (callback != null) {
1039  callback.disInformationReceived(deviceId, pair.first , pair.second);
1040  }
1041  }
1042  });
1043  } else if (uuid.equals(BlePsFtpUtils.RFC77_PFTP_SERVICE)) {
1044  BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
1045  return client.waitPsFtpClientReady(true).observeOn(scheduler).doOnComplete(new Action() {
1046  @Override
1047  public void run() throws Exception {
1048  if (callback != null &&
1049  (session.getPolarDeviceType().equals("OH1") || session.getPolarDeviceType().equals("H10"))) {
1050  callback.polarFtpFeatureReady(deviceId);
1051  }
1052  }
1053  }).toFlowable();
1054  }
1055  }
1056  return Flowable.empty();
1057  }
1058  }).subscribe(
1059  new Consumer<Object>() {
1060  @Override
1061  public void accept(Object o) throws Exception {
1062 
1063  }
1064  },
1065  new Consumer<Throwable>() {
1066  @Override
1067  public void accept(Throwable throwable) throws Exception {
1068  logError(throwable.getMessage());
1069  }
1070  },
1071  new Action() {
1072  @Override
1073  public void run() throws Exception {
1074  log("complete");
1075  }
1076  });
1077  }
1078 
1079  protected Exception handleError(Throwable throwable) {
1080  if( throwable instanceof BleDisconnected ){
1081  return new PolarDeviceDisconnected();
1082  } else {
1083  return new Exception("Unknown Error: " + throwable.getLocalizedMessage());
1084  }
1085  }
1086 
1087  interface FetchRecursiveCondition {
1088  boolean include(String entry);
1089  }
1090 
1091  protected Flowable<String> fetchRecursively(final BlePsFtpClient client, final String path, final FetchRecursiveCondition condition) {
1092  protocol.PftpRequest.PbPFtpOperation.Builder builder = protocol.PftpRequest.PbPFtpOperation.newBuilder();
1093  builder.setCommand(PftpRequest.PbPFtpOperation.Command.GET);
1094  builder.setPath(path);
1095  return client.request(builder.build().toByteArray()).toFlowable().flatMap(new Function<ByteArrayOutputStream, Publisher<String>>() {
1096  @Override
1097  public Publisher<String> apply(ByteArrayOutputStream byteArrayOutputStream) throws Exception {
1098  PftpResponse.PbPFtpDirectory dir = PftpResponse.PbPFtpDirectory.parseFrom(byteArrayOutputStream.toByteArray());
1099  Set<String> entrys = new HashSet<>();
1100  for( int i=0; i < dir.getEntriesCount(); ++i ){
1101  PftpResponse.PbPFtpEntry entry = dir.getEntries(i);
1102  if( condition.include(entry.getName()) ){
1103  BleUtils.validate(entrys.add(path + entry.getName()),"duplicate entry");
1104  }
1105  }
1106  if(entrys.size()!=0) {
1107  return Flowable.fromIterable(entrys).flatMap(new Function<String, Publisher<String>>() {
1108  @Override
1109  public Publisher<String> apply(String s) {
1110  if (s.endsWith("/")) {
1111  return fetchRecursively(client, s, condition);
1112  } else {
1113  return Flowable.just(s);
1114  }
1115  }
1116  });
1117  }
1118  return Flowable.empty();
1119  }
1120  });
1121  }
1122 
1123  protected void log(final String message) {
1124  if(logger != null){
1125  logger.message("" + message);
1126  }
1127  }
1128 
1129  protected void logError(final String message) {
1130  if(logger != null){
1131  logger.message("Error: "+message);
1132  }
1133  }
1134 }
-
void log(final String message)
+Go to the documentation of this file.
1 // Copyright © 2019 Polar Electro Oy. All rights reserved.
2 package polar.com.sdk.impl;
3 
4 import android.annotation.SuppressLint;
5 import android.bluetooth.le.ScanFilter;
6 import android.content.Context;
7 import android.os.Build;
8 import android.os.ParcelUuid;
9 import android.support.annotation.Nullable;
10 import android.util.Log;
11 import android.util.Pair;
12 
13 import com.androidcommunications.polar.api.ble.BleDeviceListener;
14 import com.androidcommunications.polar.api.ble.BleLogger;
15 import com.androidcommunications.polar.api.ble.exceptions.BleDisconnected;
16 import com.androidcommunications.polar.api.ble.model.BleDeviceSession;
17 import com.androidcommunications.polar.api.ble.model.advertisement.BleAdvertisementContent;
18 import com.androidcommunications.polar.api.ble.model.advertisement.BlePolarHrAdvertisement;
19 import com.androidcommunications.polar.api.ble.model.gatt.BleGattBase;
20 import com.androidcommunications.polar.api.ble.model.gatt.client.BleBattClient;
21 import com.androidcommunications.polar.api.ble.model.gatt.client.BleDisClient;
22 import com.androidcommunications.polar.api.ble.model.gatt.client.BleHrClient;
23 import com.androidcommunications.polar.api.ble.model.gatt.client.BlePMDClient;
24 import com.androidcommunications.polar.api.ble.model.gatt.client.psftp.BlePsFtpClient;
25 import com.androidcommunications.polar.api.ble.model.gatt.client.psftp.BlePsFtpUtils;
26 import com.androidcommunications.polar.common.ble.BleUtils;
27 import com.androidcommunications.polar.enpoints.ble.bluedroid.host.BDDeviceListenerImpl;
28 
29 import org.reactivestreams.Publisher;
30 
31 import java.io.ByteArrayOutputStream;
32 import java.text.SimpleDateFormat;
33 import java.util.ArrayList;
34 import java.util.Calendar;
35 import java.util.Collections;
36 import java.util.Comparator;
37 import java.util.Date;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Locale;
42 import java.util.Map;
43 import java.util.Set;
44 import java.util.UUID;
45 import java.util.concurrent.TimeUnit;
46 import java.util.concurrent.atomic.AtomicInteger;
47 
48 import fi.polar.remote.representation.protobuf.ExerciseSamples;
49 import fi.polar.remote.representation.protobuf.Types;
50 import io.reactivex.Completable;
51 import io.reactivex.CompletableEmitter;
52 import io.reactivex.CompletableOnSubscribe;
53 import io.reactivex.CompletableSource;
54 import io.reactivex.Flowable;
55 import io.reactivex.Scheduler;
56 import io.reactivex.Single;
57 import io.reactivex.SingleSource;
58 import io.reactivex.android.schedulers.AndroidSchedulers;
59 import io.reactivex.disposables.Disposable;
60 import io.reactivex.functions.Action;
61 import io.reactivex.functions.BiFunction;
62 import io.reactivex.functions.Consumer;
63 import io.reactivex.functions.Function;
64 import io.reactivex.functions.Predicate;
65 import io.reactivex.schedulers.Timed;
66 import polar.com.sdk.api.PolarBleApi;
86 import protocol.PftpNotification;
87 import protocol.PftpRequest;
88 import protocol.PftpResponse;
89 
93 public class BDBleApiImpl extends PolarBleApi {
94  protected final static String TAG = BDBleApiImpl.class.getSimpleName();
95  protected BleDeviceListener listener;
96  protected Map<String,Disposable> connectSubscriptions = new HashMap<>();
97  protected Scheduler scheduler;
100  protected static final int ANDROID_VERSION_O = 26;
101  BleDeviceListener.BleSearchPreFilter filter = new BleDeviceListener.BleSearchPreFilter() {
102  @Override
103  public boolean process(BleAdvertisementContent content) {
104  return content.getPolarDeviceId().length() != 0 && !content.getPolarDeviceType().equals("mobile");
105  }
106  };
107 
108  @SuppressLint({"NewApi", "CheckResult"})
109  public BDBleApiImpl(final Context context, int features) {
110  super(features);
111  Set<Class<? extends BleGattBase>> clients = new HashSet<>();
112  if((this.features & PolarBleApi.FEATURE_HR)!=0){
113  clients.add(BleHrClient.class);
114  }
115  if((this.features & PolarBleApi.FEATURE_DEVICE_INFO)!=0){
116  clients.add(BleDisClient.class);
117  }
118  if((this.features & PolarBleApi.FEATURE_BATTERY_INFO)!=0){
119  clients.add(BleBattClient.class);
120  }
121  if((this.features & PolarBleApi.FEATURE_POLAR_SENSOR_STREAMING)!=0){
122  clients.add(BlePMDClient.class);
123  }
124  if((this.features & PolarBleApi.FEATURE_POLAR_FILE_TRANSFER)!=0){
125  clients.add(BlePsFtpClient.class);
126  }
127  listener = new BDDeviceListenerImpl(context, clients);
128  listener.setScanPreFilter(filter);
129  scheduler = AndroidSchedulers.from(context.getMainLooper());
130  listener.monitorDeviceSessionState(null).observeOn(scheduler).subscribe(
131  new Consumer<Pair<BleDeviceSession, BleDeviceSession.DeviceSessionState>>() {
132  @Override
133  public void accept(Pair<BleDeviceSession, BleDeviceSession.DeviceSessionState> pair) throws Exception {
134  PolarDeviceInfo info = new PolarDeviceInfo(
135  pair.first.getPolarDeviceId().length() != 0 ?
136  pair.first.getPolarDeviceId() : pair.first.getAddress(),
137  pair.first.getAddress(),
138  pair.first.getRssi(),pair.first.getName(),true);
139  switch (pair.second){
140  case SESSION_OPEN:
141  if(callback!=null){
143  }
144  setupDevice(pair.first);
145  break;
146  case SESSION_CLOSED:
147  if( callback != null ) {
148  if (pair.first.getPreviousState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN ||
149  pair.first.getPreviousState() == BleDeviceSession.DeviceSessionState.SESSION_CLOSING){
151  }
152  }
153  break;
154  case SESSION_OPENING:
155  if(callback != null){
157  }
158  break;
159  }
160  }
161  },
162  new Consumer<Throwable>() {
163  @Override
164  public void accept(Throwable throwable) throws Exception {
165  logError(throwable.getMessage());
166  }
167  },
168  new Action() {
169  @Override
170  public void run() throws Exception {
171 
172  }
173  }
174  );
175  listener.monitorBleState().observeOn(scheduler).subscribe(
176  new Consumer<Boolean>() {
177  @Override
178  public void accept(Boolean aBoolean) throws Exception {
179  if(callback != null){
180  callback.blePowerStateChanged(aBoolean);
181  }
182  }
183  },
184  new Consumer<Throwable>() {
185  @Override
186  public void accept(Throwable throwable) throws Exception {
187  logError(throwable.getMessage());
188  }
189  },
190  new Action() {
191  @Override
192  public void run() throws Exception {
193 
194  }
195  }
196  );
197  BleLogger.setLoggerInterface(new BleLogger.BleLoggerInterface() {
198  @Override
199  public void d(String tag, String msg) {
200  log(tag+"/"+msg);
201  }
202 
203  @Override
204  public void e(String tag, String msg) {
205  logError(tag+"/"+msg);
206  }
207 
208  @Override
209  public void w(String tag, String msg) {
210  }
211 
212  @Override
213  public void i(String tag, String msg) {
214  }
215  });
216  }
217 
218  @SuppressLint("NewApi")
219  protected void enableAndroidScanFilter() {
220  if (Build.VERSION.SDK_INT >= ANDROID_VERSION_O) {
221  List<ScanFilter> filter = new ArrayList<>();
222  filter.add(new ScanFilter.Builder().setServiceUuid(
223  ParcelUuid.fromString(BleHrClient.HR_SERVICE.toString())).build());
224  filter.add(new ScanFilter.Builder().setServiceUuid(
225  ParcelUuid.fromString(BlePsFtpUtils.RFC77_PFTP_SERVICE.toString())).build());
226  listener.setScanFilters(filter);
227  }
228  }
229 
230  @Override
231  public void shutDown() {
232  listener.shutDown();
233  }
234 
235  @Override
236  public void cleanup() {
237  listener.removeAllSessions();
238  }
239 
240  @Override
241  public void setPolarFilter(boolean enable) {
242  if(!enable) {
243  listener.setScanPreFilter(null);
244  } else {
245  listener.setScanPreFilter(filter);
246  }
247  }
248 
249  @Override
250  public boolean isFeatureReady(final String deviceId, int feature) {
251  try {
252  switch (feature) {
254  return sessionPsFtpClientReady(deviceId) != null;
256  return sessionPmdClientReady(deviceId) != null;
257  }
258  } catch (Throwable ignored) {
259  }
260  return false;
261  }
262 
263  @Override
265  this.callback = callback;
267  }
268 
269  @Override
270  public void setApiLogger(@Nullable PolarBleApiLogger logger) {
271  this.logger = logger;
272  }
273 
274  @Override
275  public void setAutomaticReconnection(boolean disable) {
276  listener.setAutomaticReconnection(disable);
277  }
278 
279  @Override
280  public Completable setLocalTime(String identifier, Calendar cal) {
281  try {
282  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
283  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
284  PftpRequest.PbPFtpSetLocalTimeParams.Builder builder = PftpRequest.PbPFtpSetLocalTimeParams.newBuilder();
285  Types.PbDate date = Types.PbDate.newBuilder()
286  .setYear(cal.get(Calendar.YEAR))
287  .setMonth(cal.get(Calendar.MONTH) + 1)
288  .setDay(cal.get(Calendar.DAY_OF_MONTH)).build();
289  Types.PbTime time = Types.PbTime.newBuilder()
290  .setHour(cal.get(Calendar.HOUR_OF_DAY))
291  .setMinute(cal.get(Calendar.MINUTE))
292  .setSeconds(cal.get(Calendar.SECOND))
293  .setMillis(cal.get(Calendar.MILLISECOND)).build();
294  builder.setDate(date).setTime(time).setTzOffset((int) TimeUnit.MINUTES.convert(cal.get(Calendar.ZONE_OFFSET), TimeUnit.MILLISECONDS));
295  return client.query(PftpRequest.PbPFtpQuery.SET_LOCAL_TIME_VALUE,builder.build().toByteArray()).toObservable().ignoreElements();
296  } catch (Throwable error){
297  return Completable.error(error);
298  }
299  }
300 
301  @Override
302  public Single<PolarSensorSetting> requestAccSettings(String identifier) {
303  return querySettings(identifier,BlePMDClient.PmdMeasurementType.ACC);
304  }
305 
306  @Override
307  public Single<PolarSensorSetting> requestEcgSettings(String identifier) {
308  return querySettings(identifier,BlePMDClient.PmdMeasurementType.ECG);
309  }
310 
311  @Override
312  public Single<PolarSensorSetting> requestPpgSettings(String identifier) {
313  return querySettings(identifier,BlePMDClient.PmdMeasurementType.PPG);
314  }
315 
316  @Override
317  public Single<PolarSensorSetting> requestBiozSettings(final String identifier){
318  return querySettings(identifier,BlePMDClient.PmdMeasurementType.BIOZ);
319  }
320 
321  protected Single<PolarSensorSetting> querySettings(final String identifier, final BlePMDClient.PmdMeasurementType type) {
322  try {
323  final BleDeviceSession session = sessionPmdClientReady(identifier);
324  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
325  return client.querySettings(type).map(new Function<BlePMDClient.PmdSetting, PolarSensorSetting>() {
326  @Override
327  public PolarSensorSetting apply(BlePMDClient.PmdSetting setting) throws Exception {
328  return new PolarSensorSetting(setting.settings, type);
329  }
330  });
331  } catch (Throwable e){
332  return Single.error(e);
333  }
334  }
335 
336  @Override
337  public void backgroundEntered() {
339  }
340 
341  @Override
342  public void foregroundEntered() {
343  listener.setScanFilters(null);
344  }
345 
346  @Override
347  public Completable autoConnectToDevice(final int rssiLimit, final String service, final int timeout, final TimeUnit unit, final String polarDeviceType) {
348  final long[] start = {0};
349  return Completable.create(new CompletableOnSubscribe() {
350  @Override
351  public void subscribe(CompletableEmitter emitter) throws Exception {
352  if( service == null || service.matches("([0-9a-fA-F]{4})") ) {
353  emitter.onComplete();
354  } else {
355  emitter.tryOnError(new PolarInvalidArgument("Invalid service string format"));
356  }
357  }
358  }).andThen(listener.search(false).filter(new Predicate<BleDeviceSession>() {
359  @Override
360  public boolean test(BleDeviceSession bleDeviceSession) throws Exception {
361  if( bleDeviceSession.getMedianRssi() >= rssiLimit &&
362  bleDeviceSession.isConnectableAdvertisement() &&
363  (polarDeviceType == null || polarDeviceType.equals(bleDeviceSession.getPolarDeviceType())) &&
364  (service == null || bleDeviceSession.getAdvertisementContent().containsService(service)) ) {
365  if(start[0] == 0){
366  start[0] = System.currentTimeMillis();
367  }
368  return true;
369  }
370  return false;
371  }
372  }).timestamp().takeUntil(new Predicate<Timed<BleDeviceSession>>() {
373  @Override
374  public boolean test(Timed<BleDeviceSession> bleDeviceSessionTimed) throws Exception {
375  long diff = bleDeviceSessionTimed.time(TimeUnit.MILLISECONDS) - start[0];
376  return (diff >= unit.toMillis(timeout));
377  }
378  }).reduce(new HashSet<BleDeviceSession>(), new BiFunction<Set<BleDeviceSession>, Timed<BleDeviceSession>, Set<BleDeviceSession>>() {
379  @Override
380  public Set<BleDeviceSession> apply(Set<BleDeviceSession> objects, Timed<BleDeviceSession> bleDeviceSessionTimed) throws Exception {
381  objects.add(bleDeviceSessionTimed.value());
382  return objects;
383  }
384  }).doOnSuccess(new Consumer<Set<BleDeviceSession>>() {
385  @Override
386  public void accept(Set<BleDeviceSession> set) throws Exception {
387  List<BleDeviceSession> list = new ArrayList<>(set);
388  Collections.sort(list, new Comparator<BleDeviceSession>() {
389  @Override
390  public int compare(BleDeviceSession o1, BleDeviceSession o2) {
391  return o1.getRssi() > o2.getRssi() ? -1 : 1;
392  }
393  });
394  listener.openSessionDirect(list.get(0));
395  log("auto connect search complete");
396  }
397  }).toObservable().ignoreElements());
398  }
399 
400  @Override
401  public Completable autoConnectToDevice(final int rssiLimit, final String service, final String polarDeviceType) {
402  return autoConnectToDevice(rssiLimit, service, 2, TimeUnit.SECONDS, polarDeviceType);
403  }
404 
405  @Override
406  public void connectToDevice(final String identifier) throws PolarInvalidArgument {
407  BleDeviceSession session = fetchSession(identifier);
408  if( session == null || session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_CLOSED ){
409  if( connectSubscriptions.containsKey(identifier) ){
410  connectSubscriptions.get(identifier).dispose();
411  connectSubscriptions.remove(identifier);
412  }
413  if( session != null ){
414  listener.openSessionDirect(session);
415  } else {
416  connectSubscriptions.put(identifier, listener.search(false).filter(new Predicate<BleDeviceSession>() {
417  @Override
418  public boolean test(BleDeviceSession bleDeviceSession) throws Exception {
419  return identifier.contains(":") ?
420  bleDeviceSession.getAddress().equals(identifier) :
421  bleDeviceSession.getPolarDeviceId().equals(identifier);
422  }
423  }).take(1).observeOn(scheduler).subscribe(
424  new Consumer<BleDeviceSession>() {
425  @Override
426  public void accept(BleDeviceSession bleDeviceSession) throws Exception {
427  listener.openSessionDirect(bleDeviceSession);
428  }
429  },
430  new Consumer<Throwable>() {
431  @Override
432  public void accept(Throwable throwable) throws Exception {
433  logError(throwable.getMessage());
434  }
435  },
436  new Action() {
437  @Override
438  public void run() throws Exception {
439  log("connect search complete");
440  }
441  }
442  ));
443  }
444  }
445  }
446 
447  @Override
448  public void disconnectFromDevice(String identifier) throws PolarInvalidArgument {
449  BleDeviceSession session = fetchSession(identifier);
450  if( session != null ){
451  if( session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN ||
452  session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPENING ||
453  session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK ) {
454  listener.closeSessionDirect(session);
455  }
456  }
457  if (connectSubscriptions.containsKey(identifier)){
458  connectSubscriptions.get(identifier).dispose();
459  connectSubscriptions.remove(identifier);
460  }
461  }
462 
463  @Override
464  public Completable startRecording(String identifier, String exerciseId, RecordingInterval interval, SampleType type) {
465  try {
466  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
467  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
468  if(session.getPolarDeviceType().equals("H10")) {
469  Types.PbSampleType t = type == SampleType.HR ?
470  Types.PbSampleType.SAMPLE_TYPE_HEART_RATE :
471  Types.PbSampleType.SAMPLE_TYPE_RR_INTERVAL;
472  Types.PbDuration duration = Types.PbDuration.newBuilder().setSeconds(interval.getValue()).build();
473  PftpRequest.PbPFtpRequestStartRecordingParams params = PftpRequest.PbPFtpRequestStartRecordingParams.newBuilder().
474  setSampleDataIdentifier(exerciseId).setSampleType(t).setRecordingInterval(duration).build();
475  return client.query(PftpRequest.PbPFtpQuery.REQUEST_START_RECORDING_VALUE, params.toByteArray()).toObservable().ignoreElements().onErrorResumeNext(new Function<Throwable, CompletableSource>() {
476  @Override
477  public CompletableSource apply(Throwable throwable) throws Exception {
478  return Completable.error(throwable);
479  }
480  });
481  }
482  return Completable.error(new PolarOperationNotSupported());
483  } catch (Throwable error){
484  return Completable.error(error);
485  }
486  }
487 
488  @Override
489  public Completable stopRecording(String identifier) {
490  try {
491  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
492  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
493  if(session.getPolarDeviceType().equals("H10")) {
494  return client.query(PftpRequest.PbPFtpQuery.REQUEST_STOP_RECORDING_VALUE, null).toObservable().ignoreElements().onErrorResumeNext(new Function<Throwable, CompletableSource>() {
495  @Override
496  public CompletableSource apply(Throwable throwable) throws Exception {
497  return Completable.error(handleError(throwable));
498  }
499  });
500  }
501  return Completable.error(new PolarOperationNotSupported());
502  } catch (Throwable error){
503  return Completable.error(error);
504  }
505  }
506 
507  @Override
508  public Single<Pair<Boolean,String>> requestRecordingStatus(String identifier) {
509  try {
510  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
511  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
512  if(session.getPolarDeviceType().equals("H10")) {
513  return client.query(PftpRequest.PbPFtpQuery.REQUEST_RECORDING_STATUS_VALUE, null).map(new Function<ByteArrayOutputStream, Pair<Boolean,String>>() {
514  @Override
515  public Pair<Boolean,String> apply(ByteArrayOutputStream byteArrayOutputStream) throws Exception {
516  PftpResponse.PbRequestRecordingStatusResult result = PftpResponse.PbRequestRecordingStatusResult.parseFrom(byteArrayOutputStream.toByteArray());
517  return new Pair<>(result.getRecordingOn(),result.hasSampleDataIdentifier() ? result.getSampleDataIdentifier() : "");
518  }
519  }).onErrorResumeNext(new Function<Throwable, SingleSource<? extends Pair<Boolean, String>>>() {
520  @Override
521  public SingleSource<? extends Pair<Boolean, String>> apply(Throwable throwable) throws Exception {
522  return Single.error(handleError(throwable));
523  }
524  });
525  }
526  return Single.error(new PolarOperationNotSupported());
527  } catch (Throwable error){
528  return Single.error(error);
529  }
530  }
531 
532  @Override
533  public Flowable<PolarExerciseEntry> listExercises(String identifier) {
534  try{
535  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
536  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
537  switch (session.getPolarDeviceType()) {
538  case "OH1":
539  return fetchRecursively(client, "/U/0/", new FetchRecursiveCondition() {
540  @Override
541  public boolean include(String entry) {
542  return entry.matches("^([0-9]{8})(\\/)") ||
543  entry.matches("^([0-9]{6})(\\/)") ||
544  entry.equals("E/") ||
545  entry.equals("SAMPLES.BPB") ||
546  entry.equals("00/");
547  }
548  }).map(new Function<String, PolarExerciseEntry>() {
549  @Override
550  public PolarExerciseEntry apply(String p) throws Exception {
551  String components[] = p.split("/");
552  SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd HHmmss", Locale.getDefault());
553  Date date = format.parse(components[3] + " " + components[5]);
554  return new PolarExerciseEntry(p, date, components[3] + components[5]);
555  }
556  }).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarExerciseEntry>>() {
557  @Override
558  public Publisher<? extends PolarExerciseEntry> apply(Throwable throwable) throws Exception {
559  return Flowable.error(handleError(throwable));
560  }
561  });
562  case "H10":
563  return fetchRecursively(client, "/", new FetchRecursiveCondition() {
564  @Override
565  public boolean include(String entry) {
566  return entry.endsWith("/") || entry.equals("SAMPLES.BPB");
567  }
568  }).map(new Function<String, PolarExerciseEntry>() {
569  @Override
570  public PolarExerciseEntry apply(String p) throws Exception {
571  String components[] = p.split("/");
572  return new PolarExerciseEntry(p, new Date(), components[1]);
573  }
574  }).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarExerciseEntry>>() {
575  @Override
576  public Publisher<? extends PolarExerciseEntry> apply(Throwable throwable) throws Exception {
577  return Flowable.error(handleError(throwable));
578  }
579  });
580  default:
581  return Flowable.error(new PolarOperationNotSupported());
582  }
583  } catch (Throwable error){
584  return Flowable.error(error);
585  }
586  }
587 
588  @Override
589  public Single<PolarExerciseData> fetchExercise(String identifier, PolarExerciseEntry entry) {
590  try{
591  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
592  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
593  protocol.PftpRequest.PbPFtpOperation.Builder builder = protocol.PftpRequest.PbPFtpOperation.newBuilder();
594  builder.setCommand(PftpRequest.PbPFtpOperation.Command.GET);
595  builder.setPath(entry.path);
596  if(session.getPolarDeviceType().equals("OH1") || session.getPolarDeviceType().equals("H10")) {
597  return client.request(builder.build().toByteArray()).map(new Function<ByteArrayOutputStream, PolarExerciseData>() {
598  @Override
599  public PolarExerciseData apply(ByteArrayOutputStream byteArrayOutputStream) throws Exception {
600  ExerciseSamples.PbExerciseSamples samples = ExerciseSamples.PbExerciseSamples.parseFrom(byteArrayOutputStream.toByteArray());
601  if(samples.hasRrSamples()){
602  return new PolarExerciseData(samples.getRecordingInterval().getSeconds(), samples.getRrSamples().getRrIntervalsList());
603  } else {
604  return new PolarExerciseData(samples.getRecordingInterval().getSeconds(), samples.getHeartRateSamplesList());
605  }
606  }
607  }).onErrorResumeNext(new Function<Throwable, SingleSource<? extends PolarExerciseData>>() {
608  @Override
609  public SingleSource<? extends PolarExerciseData> apply(Throwable throwable) throws Exception {
610  return Single.error(handleError(throwable));
611  }
612  });
613  }
614  return Single.error(new PolarOperationNotSupported());
615  } catch (Throwable error){
616  return Single.error(error);
617  }
618  }
619 
620  @Override
621  public Completable removeExercise(String identifier, PolarExerciseEntry entry) {
622  try{
623  final BleDeviceSession session = sessionPsFtpClientReady(identifier);
624  final BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
625  if(session.getPolarDeviceType().equals("OH1")){
626  protocol.PftpRequest.PbPFtpOperation.Builder builder = protocol.PftpRequest.PbPFtpOperation.newBuilder();
627  builder.setCommand(PftpRequest.PbPFtpOperation.Command.GET);
628  final String[] components = entry.path.split("/");
629  final String exerciseParent = "/U/0/" + components[3] + "/E/";
630  builder.setPath(exerciseParent);
631  return client.request(builder.build().toByteArray()).flatMap(new Function<ByteArrayOutputStream, SingleSource<?>>() {
632  @Override
633  public SingleSource<?> apply(ByteArrayOutputStream byteArrayOutputStream) throws Exception {
634  PftpResponse.PbPFtpDirectory directory = PftpResponse.PbPFtpDirectory.parseFrom(byteArrayOutputStream.toByteArray());
635  protocol.PftpRequest.PbPFtpOperation.Builder removeBuilder = protocol.PftpRequest.PbPFtpOperation.newBuilder();
636  removeBuilder.setCommand(PftpRequest.PbPFtpOperation.Command.REMOVE);
637  if( directory.getEntriesCount() <= 1 ){
638  // remove entire directory
639  removeBuilder.setPath("/U/0/" + components[3] + "/");
640  } else {
641  // remove only exercise
642  removeBuilder.setPath("/U/0/" + components[3] + "/E/" + components[5] + "/");
643  }
644  return client.request(removeBuilder.build().toByteArray());
645  }
646  }).toObservable().ignoreElements().onErrorResumeNext(new Function<Throwable, CompletableSource>() {
647  @Override
648  public CompletableSource apply(Throwable throwable) throws Exception {
649  return Completable.error(handleError(throwable));
650  }
651  });
652  } else if(session.getPolarDeviceType().equals("H10")){
653  protocol.PftpRequest.PbPFtpOperation.Builder builder = protocol.PftpRequest.PbPFtpOperation.newBuilder();
654  builder.setCommand(PftpRequest.PbPFtpOperation.Command.REMOVE);
655  builder.setPath(entry.path);
656  return client.request(builder.build().toByteArray()).toObservable().ignoreElements().onErrorResumeNext(new Function<Throwable, CompletableSource>() {
657  @Override
658  public CompletableSource apply(Throwable throwable) throws Exception {
659  return Completable.error(handleError(throwable));
660  }
661  });
662  }
663  return Completable.error(new PolarOperationNotSupported());
664  } catch (Throwable error){
665  return Completable.error(error);
666  }
667  }
668 
669  @Override
670  public Flowable<PolarDeviceInfo> searchForDevice() {
671  return listener.search(false).distinct().map(new Function<BleDeviceSession, PolarDeviceInfo>() {
672  @Override
673  public PolarDeviceInfo apply(BleDeviceSession bleDeviceSession) throws Exception {
674  return new PolarDeviceInfo(bleDeviceSession.getPolarDeviceId(),
675  bleDeviceSession.getAddress(),
676  bleDeviceSession.getRssi(),
677  bleDeviceSession.getName(),
678  bleDeviceSession.isConnectableAdvertisement());
679  }
680  });
681  }
682 
683  @Override
684  public Flowable<PolarHrBroadcastData> startListenForPolarHrBroadcasts(final Set<String> deviceIds) {
685  // set filter to null, NOTE this disables reconnection in background
686  return listener.search(false).filter(new Predicate<BleDeviceSession>() {
687  @Override
688  public boolean test(BleDeviceSession bleDeviceSession) throws Exception {
689  return (deviceIds == null || deviceIds.contains(bleDeviceSession.getPolarDeviceId())) &&
690  bleDeviceSession.getAdvertisementContent().getPolarHrAdvertisement().isPresent();
691  }
692  }).map(new Function<BleDeviceSession, PolarHrBroadcastData>() {
693  @Override
694  public PolarHrBroadcastData apply(BleDeviceSession bleDeviceSession) throws Exception {
695  BlePolarHrAdvertisement advertisement = bleDeviceSession.getBlePolarHrAdvertisement();
696  return new PolarHrBroadcastData( new PolarDeviceInfo(bleDeviceSession.getPolarDeviceId(),
697  bleDeviceSession.getAddress(),
698  bleDeviceSession.getRssi(),
699  bleDeviceSession.getName(),
700  bleDeviceSession.isConnectableAdvertisement()),
701  advertisement.getHrForDisplay(),
702  advertisement.getBatteryStatus() != 0);
703  }
704  });
705  }
706 
707  @Override
708  public Flowable<PolarEcgData> startEcgStreaming(String identifier,
709  PolarSensorSetting setting) {
710  try {
711  final BleDeviceSession session = sessionPmdClientReady(identifier);
712  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
713  return client.startMeasurement(BlePMDClient.PmdMeasurementType.ECG, setting.map2PmdSettings()).andThen(
714  client.monitorEcgNotifications(true).map(new Function<BlePMDClient.EcgData, PolarEcgData>() {
715  @Override
716  public PolarEcgData apply(BlePMDClient.EcgData ecgData) throws Exception {
717  List<Integer> samples = new ArrayList<>();
718  for( BlePMDClient.EcgData.EcgSample s : ecgData.ecgSamples ){
719  samples.add(s.microVolts);
720  }
721  return new PolarEcgData(samples,ecgData.timeStamp);
722  }
723  }).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarEcgData>>() {
724  @Override
725  public Publisher<? extends PolarEcgData> apply(Throwable throwable) throws Exception {
726  return Flowable.error(handleError(throwable));
727  }
728  }).doFinally(new Action() {
729  @Override
730  public void run() throws Exception {
731  stopPmdStreaming(session,client, BlePMDClient.PmdMeasurementType.ECG);
732  }
733  }));
734  } catch (Throwable t){
735  return Flowable.error(t);
736  }
737  }
738 
739  @Override
740  public Flowable<PolarAccelerometerData> startAccStreaming(String identifier,
741  PolarSensorSetting setting) {
742  try {
743  final BleDeviceSession session = sessionPmdClientReady(identifier);
744  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
745  return client.startMeasurement(BlePMDClient.PmdMeasurementType.ACC, setting.map2PmdSettings()).andThen(
746  client.monitorAccNotifications(true).map(new Function<BlePMDClient.AccData, PolarAccelerometerData>() {
747  @Override
748  public PolarAccelerometerData apply(BlePMDClient.AccData accData) throws Exception {
749  List<PolarAccelerometerData.PolarAccelerometerSample> samples = new ArrayList<>();
750  for( BlePMDClient.AccData.AccSample s : accData.accSamples ){
751  samples.add(new PolarAccelerometerData.PolarAccelerometerSample(s.x,s.y,s.z));
752  }
753  return new PolarAccelerometerData(samples,accData.timeStamp);
754  }
755  }).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarAccelerometerData>>() {
756  @Override
757  public Publisher<? extends PolarAccelerometerData> apply(Throwable throwable) throws Exception {
758  return Flowable.error(handleError(throwable));
759  }
760  }).doFinally(new Action() {
761  @Override
762  public void run() throws Exception {
763  stopPmdStreaming(session,client, BlePMDClient.PmdMeasurementType.ACC);
764  }
765  }));
766  } catch (Throwable t){
767  return Flowable.error(t);
768  }
769  }
770 
771  @Override
772  public Flowable<PolarOhrPPGData> startOhrPPGStreaming(String identifier,
773  PolarSensorSetting setting) {
774  try {
775  final BleDeviceSession session = sessionPmdClientReady(identifier);
776  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
777  return client.startMeasurement(BlePMDClient.PmdMeasurementType.PPG, setting.map2PmdSettings()).andThen(
778  client.monitorPpgNotifications(true).map(new Function<BlePMDClient.PpgData, PolarOhrPPGData>() {
779  @Override
780  public PolarOhrPPGData apply(BlePMDClient.PpgData ppgData) throws Exception {
781  List<PolarOhrPPGData.PolarOhrPPGSample> samples = new ArrayList<>();
782  for( BlePMDClient.PpgData.PpgSample s : ppgData.ppgSamples ){
783  samples.add(new PolarOhrPPGData.PolarOhrPPGSample(s.ppg0,s.ppg1,s.ppg2,s.ambient,s.ppgDataSamples,s.ambient1,s.status));
784  }
785  return new PolarOhrPPGData(samples,ppgData.timeStamp);
786  }
787  }).doFinally(new Action() {
788  @Override
789  public void run() throws Exception {
790  stopPmdStreaming(session,client, BlePMDClient.PmdMeasurementType.PPG);
791  }
792  })).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarOhrPPGData>>() {
793  @Override
794  public Publisher<? extends PolarOhrPPGData> apply(Throwable throwable) throws Exception {
795  return Flowable.error(handleError(throwable));
796  }
797  });
798  } catch (Throwable t){
799  return Flowable.error(t);
800  }
801  }
802 
803  @Override
804  public Flowable<PolarOhrPPIData> startOhrPPIStreaming(String identifier) {
805  try {
806  final BleDeviceSession session = sessionPmdClientReady(identifier);
807  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
808  return client.startMeasurement(BlePMDClient.PmdMeasurementType.PPI, new BlePMDClient.PmdSetting(new HashMap<BlePMDClient.PmdSetting.PmdSettingType, Integer>())).andThen(
809  client.monitorPpiNotifications(true).map(new Function<BlePMDClient.PpiData, PolarOhrPPIData>() {
810  @Override
811  public PolarOhrPPIData apply(BlePMDClient.PpiData ppiData) throws Exception {
812  List<PolarOhrPPIData.PolarOhrPPISample> samples = new ArrayList<>();
813  for(BlePMDClient.PpiData.PPSample ppSample : ppiData.ppSamples){
814  samples.add(new PolarOhrPPIData.PolarOhrPPISample(ppSample.ppInMs,
815  ppSample.ppErrorEstimate,
816  ppSample.hr,
817  ppSample.blockerBit != 0,
818  ppSample.skinContactStatus != 0,
819  ppSample.skinContactSupported != 0));
820  }
821  return new PolarOhrPPIData(ppiData.timestamp,samples);
822  }
823  }).doFinally(new Action() {
824  @Override
825  public void run() throws Exception {
826  stopPmdStreaming(session,client, BlePMDClient.PmdMeasurementType.PPI);
827  }
828  })).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarOhrPPIData>>() {
829  @Override
830  public Publisher<? extends PolarOhrPPIData> apply(Throwable throwable) throws Exception {
831  return Flowable.error(handleError(throwable));
832  }
833  });
834  } catch (Throwable t){
835  return Flowable.error(t);
836  }
837  }
838 
839  @Override
840  public Flowable<PolarBiozData> startBiozStreaming(final String identifier, PolarSensorSetting setting){
841  try {
842  final BleDeviceSession session = sessionPmdClientReady(identifier);
843  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
844  return client.startMeasurement(BlePMDClient.PmdMeasurementType.BIOZ, setting.map2PmdSettings()).andThen(
845  client.monitorBiozNotifications(true).map(new Function<BlePMDClient.BiozData, PolarBiozData>() {
846  @Override
847  public PolarBiozData apply(BlePMDClient.BiozData biozData) throws Exception {
848  return new PolarBiozData(biozData.timeStamp,biozData.samples,biozData.status);
849  }
850  }).doFinally(new Action() {
851  @Override
852  public void run() throws Exception {
853  stopPmdStreaming(session,client, BlePMDClient.PmdMeasurementType.PPG);
854  }
855  })).onErrorResumeNext(new Function<Throwable, Publisher<? extends PolarBiozData>>() {
856  @Override
857  public Publisher<? extends PolarBiozData> apply(Throwable throwable) throws Exception {
858  return Flowable.error(handleError(throwable));
859  }
860  });
861  } catch (Throwable t){
862  return Flowable.error(t);
863  }
864  }
865 
866  protected BleDeviceSession fetchSession(final String identifier) throws PolarInvalidArgument {
867  if(identifier.matches("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")){
868  return sessionByAddress(identifier);
869  } else if(identifier.matches("([0-9a-fA-F]){6,8}")) {
870  return sessionByDeviceId(identifier);
871  }
872  throw new PolarInvalidArgument();
873  }
874 
875  protected BleDeviceSession sessionByAddress(final String address) throws PolarInvalidArgument {
876  for ( BleDeviceSession session : listener.deviceSessions() ){
877  if( session.getAddress().equals(address) ){
878  return session;
879  }
880  }
881  return null;
882  }
883 
884  protected BleDeviceSession sessionByDeviceId(final String deviceId) throws PolarInvalidArgument {
885  for ( BleDeviceSession session : listener.deviceSessions() ){
886  if( session.getAdvertisementContent().getPolarDeviceId().equals(deviceId) ){
887  return session;
888  }
889  }
890  return null;
891  }
892 
893  protected BleDeviceSession sessionServiceReady(final String identifier, UUID service) throws Throwable {
894  BleDeviceSession session = fetchSession(identifier);
895  if(session != null){
896  if(session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN) {
897  BleGattBase client = session.fetchClient(service);
898  if (client.isServiceDiscovered()) {
899  return session;
900  }
901  throw new PolarServiceNotAvailable();
902  }
903  throw new PolarDeviceDisconnected();
904  }
905  throw new PolarDeviceNotFound();
906  }
907 
908  public BleDeviceSession sessionPmdClientReady(final String identifier) throws Throwable {
909  BleDeviceSession session = sessionServiceReady(identifier, BlePMDClient.PMD_SERVICE);
910  BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
911  final AtomicInteger pair = client.getNotificationAtomicInteger(BlePMDClient.PMD_CP);
912  final AtomicInteger pairData = client.getNotificationAtomicInteger(BlePMDClient.PMD_DATA);
913  if (pair != null && pairData != null &&
914  pair.get() == BleGattBase.ATT_SUCCESS &&
915  pairData.get() == BleGattBase.ATT_SUCCESS) {
916  return session;
917  }
918  throw new PolarNotificationNotEnabled();
919  }
920 
921  protected BleDeviceSession sessionPsFtpClientReady(final String identifier) throws Throwable {
922  BleDeviceSession session = sessionServiceReady(identifier, BlePsFtpUtils.RFC77_PFTP_SERVICE);
923  BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
924  final AtomicInteger pair = client.getNotificationAtomicInteger(BlePsFtpUtils.RFC77_PFTP_MTU_CHARACTERISTIC);
925  if (pair != null && pair.get() == BleGattBase.ATT_SUCCESS ) {
926  return session;
927  }
928  throw new PolarNotificationNotEnabled();
929  }
930 
931  @SuppressLint("CheckResult")
932  protected void stopPmdStreaming(BleDeviceSession session, BlePMDClient client, BlePMDClient.PmdMeasurementType type) {
933  if( session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN ){
934  // stop streaming
935  client.stopMeasurement(type).subscribe(
936  new Action() {
937  @Override
938  public void run() throws Exception {
939 
940  }
941  },
942  new Consumer<Throwable>() {
943  @Override
944  public void accept(Throwable throwable) throws Exception {
945  logError("failed to stop pmd stream: " + throwable.getLocalizedMessage());
946  }
947  }
948  );
949  }
950  }
951 
952  @SuppressLint("CheckResult")
953  protected void setupDevice(final BleDeviceSession session){
954  final String deviceId = session.getPolarDeviceId().length() != 0 ? session.getPolarDeviceId() : session.getAddress();
955  session.monitorServicesDiscovered(true).observeOn(scheduler).toFlowable().flatMapIterable(
956  new Function<List<UUID>, Iterable<UUID>>() {
957  @Override
958  public Iterable<UUID> apply(List<UUID> uuids) throws Exception {
959  return uuids;
960  }
961  }
962  ).flatMap(new Function<UUID, Publisher<?>>() {
963  @Override
964  public Publisher<?> apply(UUID uuid) throws Exception {
965  if(session.fetchClient(uuid) != null) {
966  if (uuid.equals(BleHrClient.HR_SERVICE)) {
967  if (callback != null) {
968  callback.hrFeatureReady(deviceId);
969  }
970  final BleHrClient client = (BleHrClient) session.fetchClient(BleHrClient.HR_SERVICE);
971  client.observeHrNotifications(true).observeOn(scheduler).subscribe(
972  new Consumer<BleHrClient.HrNotificationData>() {
973  @Override
974  public void accept(BleHrClient.HrNotificationData hrNotificationData) throws Exception {
975  if (callback != null) {
977  new PolarHrData(hrNotificationData.hrValue,
978  hrNotificationData.rrs,
979  hrNotificationData.sensorContact,
980  hrNotificationData.sensorContactSupported,
981  hrNotificationData.rrPresent));
982  }
983  }
984  },
985  new Consumer<Throwable>() {
986  @Override
987  public void accept(Throwable throwable) throws Exception {
988  logError(throwable.getMessage());
989  }
990  },
991  new Action() {
992  @Override
993  public void run() throws Exception {
994 
995  }
996  }
997  );
998  } else if (uuid.equals(BleBattClient.BATTERY_SERVICE)) {
999  BleBattClient client = (BleBattClient) session.fetchClient(BleBattClient.BATTERY_SERVICE);
1000  client.monitorBatteryLevelUpdate(true).observeOn(scheduler).subscribe(
1001  new Consumer<Integer>() {
1002  @Override
1003  public void accept(Integer integer) throws Exception {
1004  if (callback != null) {
1005  callback.batteryLevelReceived(deviceId, integer);
1006  }
1007  }
1008  },
1009  new Consumer<Throwable>() {
1010  @Override
1011  public void accept(Throwable throwable) throws Exception {
1012  logError(throwable.getMessage());
1013  }
1014  },
1015  new Action() {
1016  @Override
1017  public void run() throws Exception {
1018  }
1019  }
1020  );
1021  } else if (uuid.equals(BlePMDClient.PMD_SERVICE)) {
1022  final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE);
1023  return client.waitNotificationEnabled(BlePMDClient.PMD_CP, true).
1024  concatWith(client.waitNotificationEnabled(BlePMDClient.PMD_DATA, true)).andThen(client.readFeature(true).doOnSuccess(new Consumer<BlePMDClient.PmdFeature>() {
1025  @Override
1026  public void accept(BlePMDClient.PmdFeature pmdFeature) {
1027  if (callback != null) {
1028  if (pmdFeature.ecgSupported) {
1029  callback.ecgFeatureReady(deviceId);
1030  }
1031  if (pmdFeature.accSupported) {
1033  }
1034  if (pmdFeature.ppgSupported) {
1035  callback.ppgFeatureReady(deviceId);
1036  }
1037  if (pmdFeature.ppiSupported) {
1038  callback.ppiFeatureReady(deviceId);
1039  }
1040  if (pmdFeature.bioZSupported) {
1041  callback.biozFeatureReady(deviceId);
1042  }
1043  }
1044  }
1045  })).toFlowable();
1046  } else if (uuid.equals(BleDisClient.DIS_SERVICE)) {
1047  BleDisClient client = (BleDisClient) session.fetchClient(BleDisClient.DIS_SERVICE);
1048  return client.observeDisInfo(true).observeOn(scheduler).doOnNext(new Consumer<Pair<UUID, String>>() {
1049  @Override
1050  public void accept(Pair<UUID, String> pair) {
1051  if (callback != null) {
1052  callback.disInformationReceived(deviceId, pair.first , pair.second);
1053  }
1054  }
1055  });
1056  } else if (uuid.equals(BlePsFtpUtils.RFC77_PFTP_SERVICE)) {
1057  BlePsFtpClient client = (BlePsFtpClient) session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE);
1058  return client.waitPsFtpClientReady(true).observeOn(scheduler).doOnComplete(new Action() {
1059  @Override
1060  public void run() throws Exception {
1061  if (callback != null &&
1062  (session.getPolarDeviceType().equals("OH1") || session.getPolarDeviceType().equals("H10"))) {
1063  callback.polarFtpFeatureReady(deviceId);
1064  }
1065  }
1066  }).toFlowable();
1067  }
1068  }
1069  return Flowable.empty();
1070  }
1071  }).subscribe(
1072  new Consumer<Object>() {
1073  @Override
1074  public void accept(Object o) throws Exception {
1075 
1076  }
1077  },
1078  new Consumer<Throwable>() {
1079  @Override
1080  public void accept(Throwable throwable) throws Exception {
1081  logError(throwable.getMessage());
1082  }
1083  },
1084  new Action() {
1085  @Override
1086  public void run() throws Exception {
1087  log("complete");
1088  }
1089  });
1090  }
1091 
1092  protected Exception handleError(Throwable throwable) {
1093  if( throwable instanceof BleDisconnected ){
1094  return new PolarDeviceDisconnected();
1095  } else {
1096  return new Exception("Unknown Error: " + throwable.getLocalizedMessage());
1097  }
1098  }
1099 
1100  interface FetchRecursiveCondition {
1101  boolean include(String entry);
1102  }
1103 
1104  protected Flowable<String> fetchRecursively(final BlePsFtpClient client, final String path, final FetchRecursiveCondition condition) {
1105  protocol.PftpRequest.PbPFtpOperation.Builder builder = protocol.PftpRequest.PbPFtpOperation.newBuilder();
1106  builder.setCommand(PftpRequest.PbPFtpOperation.Command.GET);
1107  builder.setPath(path);
1108  return client.request(builder.build().toByteArray()).toFlowable().flatMap(new Function<ByteArrayOutputStream, Publisher<String>>() {
1109  @Override
1110  public Publisher<String> apply(ByteArrayOutputStream byteArrayOutputStream) throws Exception {
1111  PftpResponse.PbPFtpDirectory dir = PftpResponse.PbPFtpDirectory.parseFrom(byteArrayOutputStream.toByteArray());
1112  Set<String> entrys = new HashSet<>();
1113  for( int i=0; i < dir.getEntriesCount(); ++i ){
1114  PftpResponse.PbPFtpEntry entry = dir.getEntries(i);
1115  if( condition.include(entry.getName()) ){
1116  BleUtils.validate(entrys.add(path + entry.getName()),"duplicate entry");
1117  }
1118  }
1119  if(entrys.size()!=0) {
1120  return Flowable.fromIterable(entrys).flatMap(new Function<String, Publisher<String>>() {
1121  @Override
1122  public Publisher<String> apply(String s) {
1123  if (s.endsWith("/")) {
1124  return fetchRecursively(client, s, condition);
1125  } else {
1126  return Flowable.just(s);
1127  }
1128  }
1129  });
1130  }
1131  return Flowable.empty();
1132  }
1133  });
1134  }
1135 
1136  protected void log(final String message) {
1137  if(logger != null){
1138  logger.message("" + message);
1139  }
1140  }
1141 
1142  protected void logError(final String message) {
1143  if(logger != null){
1144  logger.message("Error: "+message);
1145  }
1146  }
1147 }
+
void log(final String message)
void polarFtpFeatureReady(@NonNull final String identifier)
void batteryLevelReceived(@NonNull final String identifier, final int level)
@@ -80,7 +80,7 @@
void setAutomaticReconnection(boolean disable)
Single< PolarSensorSetting > requestAccSettings(String identifier)
void deviceConnected(@NonNull final PolarDeviceInfo polarDeviceInfo)
-
Exception handleError(Throwable throwable)
+
Exception handleError(Throwable throwable)
Flowable< PolarOhrPPIData > startOhrPPIStreaming(String identifier)
void hrFeatureReady(@NonNull final String identifier)
@@ -131,7 +131,7 @@
BleDeviceSession sessionPmdClientReady(final String identifier)
-
Flowable< String > fetchRecursively(final BlePsFtpClient client, final String path, final FetchRecursiveCondition condition)
+
Flowable< String > fetchRecursively(final BlePsFtpClient client, final String path, final FetchRecursiveCondition condition)
Completable stopRecording(String identifier)
void biozFeatureReady(@NonNull final String identifier)
@@ -172,7 +172,7 @@
Completable setLocalTime(String identifier, Calendar cal)
BleDeviceSession sessionPsFtpClientReady(final String identifier)
-
void logError(final String message)
+
void logError(final String message)
diff --git a/polar-sdk-android/docs/html/classpolar_1_1com_1_1sdk_1_1api_1_1model_1_1PolarOhrPPIData.html b/polar-sdk-android/docs/html/classpolar_1_1com_1_1sdk_1_1api_1_1model_1_1PolarOhrPPIData.html index 27cedd25..2e817902 100644 --- a/polar-sdk-android/docs/html/classpolar_1_1com_1_1sdk_1_1api_1_1model_1_1PolarOhrPPIData.html +++ b/polar-sdk-android/docs/html/classpolar_1_1com_1_1sdk_1_1api_1_1model_1_1PolarOhrPPIData.html @@ -160,7 +160,7 @@

-

Last sample timestamp in nanoseconds

+

N/A always 0

Definition at line 57 of file PolarOhrPPIData.java.

diff --git a/polar-sdk-android/docs/html/classpolar_1_1com_1_1sdk_1_1impl_1_1BDBleApiImpl.html b/polar-sdk-android/docs/html/classpolar_1_1com_1_1sdk_1_1impl_1_1BDBleApiImpl.html index 548983c6..d0e33484 100644 --- a/polar-sdk-android/docs/html/classpolar_1_1com_1_1sdk_1_1impl_1_1BDBleApiImpl.html +++ b/polar-sdk-android/docs/html/classpolar_1_1com_1_1sdk_1_1impl_1_1BDBleApiImpl.html @@ -649,7 +649,7 @@

-

Definition at line 1091 of file BDBleApiImpl.java.

+

Definition at line 1104 of file BDBleApiImpl.java.

@@ -732,7 +732,7 @@

-

Definition at line 1079 of file BDBleApiImpl.java.

+

Definition at line 1092 of file BDBleApiImpl.java.

@@ -826,7 +826,7 @@

-

Definition at line 1123 of file BDBleApiImpl.java.

+

Definition at line 1136 of file BDBleApiImpl.java.

@@ -854,7 +854,7 @@

-

Definition at line 1129 of file BDBleApiImpl.java.

+

Definition at line 1142 of file BDBleApiImpl.java.