Background

While working on a project that involved wearables one of the features we were tasked with implementing was step tracking. After some research we discovered that the best way for us to implement this particular feature was to Google Fit API.

Using the Google Fit API helped us save a large amount of time on having to work on our own back end implementation for steps. Using the Fit API also provides the users access to all the useful features provided by the Google Fit APP.

This article will cover the logic for integrating the Google Fit API with a BLE device to handle steps data sync and use the Google Fit API as a cloud storage for the steps data.

The Architecture

The simple idea behind the architecture shown in the diagram above was to have the following three main components:

  • BleManager: the BLE manager is responsible for all the BLE communication with the wearable device and to provide a simple RxJava backed API to send and receive data from the device.

  • FitManager: the fit manager is responsible for handling all the communication with the Google Fit API, it also provides a simple RxJava backed APIs to read and write steps data to the Fit API.

  • StepsSyncManager: the steps sync manager is responsible for connecting the BleManager and the FitManager together by retrieving all the steps data from the BleManager and passing them to the FitManager to be stored using the Google Fit History API.

The Implementation

FitManager

When implementing the Google Fit API we wanted to use RxJava to simplify some of the complicated APIs that would be commonly used. This led to the creation of FitManager.

Connecting to the Google Fit API

To start using the Google Fit API we first have to create a new Google API client, this can be done by using the Google Fit API client builder as shown below.

private void buildClient() {
    fitApiClient = new GoogleApiClient.Builder(application)
            .addApi(Fitness.HISTORY_API)
            .addScope(new Scope(Scopes.FITNESS_ACTIVITY_READ_WRITE))
            .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
                @Override
                public void onConnected(@Nullable Bundle bundle) {
                    connectionResultPublishSubject.onNext(ConnectionUpdate.onConnected());
                }

                @Override
                public void onConnectionSuspended(int i) {
                    connectionResultPublishSubject.onNext(ConnectionUpdate.onSuspended(i));
                }
            })
            .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {
                @Override
                public void onConnectionFailed(@NonNull ConnectionResult result) {
                    connectionResultPublishSubject.onNext(ConnectionUpdate.onFailed(result));
                }
            })
            .build();
}

As shown above, to build the API client we have to specify a couple of values.

  • The first value is the API we want to use, in our case we wanted to use the Google Fit history API which can be specified using the Fitness.HISTORY_API constant.

  • We also have to specify the access scope we want to that particular API, in our case we need both read and write access to the history API to be able to save new steps data and read them again later. This is specified using the constant value Scopes.FITNESS_ACTIVITY_READ_WRITE.

  • Finally we need to provide a connection callbacks listener and connection failed listener. In the example above we are taking the results from these two listeners and then passing them to an RxJava PublishSubject. This gives us the ability to subscribe to these updates anywhere in the app where information about the connection state is required.

Retrieving total steps recorded between two timestamps

After connecting to the Google Fit API client we can start retrieving the steps data previously recorded. The method below is used to retrieve the total steps recorded between two timestamps and returning them as an RxJava observable.

public Observable<DataReadResult> getTotalStepsCount(final long startTime, final long endTime) {
    return Observable.create(new Observable.OnSubscribe<DataReadResult>() {
        @Override
        public void call(Subscriber<? super DataReadResult> subscriber) {
            DataReadResult result = Fitness.HistoryApi.readData(fitApiClient, new DataReadRequest.Builder()
                    .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
                    .bucketByTime(1, TimeUnit.DAYS)
                    .aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA)
                    .build()).await(30, TimeUnit.SECONDS);

            if (result.getStatus().isSuccess()) {
                subscriber.onNext(result);
                subscriber.onCompleted();
            } else {
                subscriber.onError(new FitApiException(result.getStatus()));
            }
        }
    });
}

Retrieving total steps recorded for the day

Sometimes we also want to retrieve the total steps recorded for the current data, thankfully the Fit API provides a simple way to retrieve the daily total as shown below.

public Observable<DailyTotalResult> getDailyStepsCount() {
    return Observable.create(new Observable.OnSubscribe<DailyTotalResult>() {
        @Override
        public void call(Subscriber<? super DailyTotalResult> subscriber) {
            DailyTotalResult result = Fitness.HistoryApi.readDailyTotal(fitApiClient, DataType.TYPE_STEP_COUNT_DELTA)
                    .await(30, TimeUnit.SECONDS);

            if (result.getStatus().isSuccess()) {
                subscriber.onNext(result);
                subscriber.onCompleted();
            } else {
                subscriber.onError(new FitApiException(result.getStatus()));
            }
        }
    });
}

Inserting new steps

Finally we need the ability to insert new steps data, the methods below allow us to insert steps data for either a single data point or multiple data points. Each data point consist of the following:

  • startTime: the start timestamp for when these steps were recorded.

  • endTime: the end timestamp for when these steps were recorded.

  • steps: the total steps recorded steps between the specified timestamps.

public Observable<Status> insertNewSteps(final long startTime, final long endTime, final int steps) {
    return insertNewSteps(Collections.singletonList(new StepsDataPoint(startTime, endTime, steps)));
}

public Observable<Status> insertNewSteps(final List<StepsDataPoint> steps) {
    return Observable.create(new Observable.OnSubscribe<Status>() {
        @Override
        public void call(Subscriber<? super Status> subscriber) {
            Status status = Fitness.HistoryApi.insertData(fitApiClient, getDataSet(steps))
                    .await(1, TimeUnit.MINUTES);

            if (status.isSuccess()) {
                subscriber.onNext(status);
                subscriber.onCompleted();
            } else {
                subscriber.onError(new FitApiException(status));
            }
        }
    });
}

BleManager

The BleManager is responsible for providing the ability to easily connect to the BLE device, send commands, and retrieve data from the device. To Simplify our BleManager implementation we looked for a good library that wraps around the core Android BLE APIs and provides RxJava support. After some research we decided to go with RxAndroidBLE, a really powerful library that removes a lot of the headache that comes with using the core Android BLE APIs.

Sending commands

To send commands to the BLE device we first need to retrieve the BLE characteristic we want to send the commands to. After retrieving the characteristic we have to specify the following two values:

Finally after specifying the write type and command value we can send back the characteristic to the BLE device as shown below.

private Observable<BluetoothGattCharacteristic> sendCommand(final byte[] command) {
    return sendCommand(command, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
}

private Observable<BluetoothGattCharacteristic> sendCommand(final byte[] command, final int type) {
    return bleConnection.getCharacteristic(COMMANDS_CHARACTERISTIC)
            .flatMap(new Func1<BluetoothGattCharacteristic, Observable<BluetoothGattCharacteristic>>() {
                @Override
                public Observable<BluetoothGattCharacteristic> call(BluetoothGattCharacteristic characteristic) {
                    characteristic.setWriteType(type);
                    characteristic.setValue(command);
                    return bleConnection.writeCharacteristic(characteristic);
                }
            });
}

Receiving data from BLE notifications

Data are sent from the BLE device using GATT notifications, so when we want to receive data from the BLE device we would setup a notification to a specific BLE characteristic and listen to its value changes. Due to the BLE limitation around the size of the data allowed to be transferred at a time, some responses have to be split into multiple parts with a header and trailer used to indicate the start and end of the transfer.

The example below shows the code for setting up notifications that will listen to steps data responses for a specific date. These responses come in multiple parts so they are being combined into a single object and then published using an RxJava PublishSubject.

private void subscribeToBleDataNotifications() {
    bleConnection.setupNotification(DATA_CHARACTERISTIC)
            .observeOn(ioSched)
            .flatMap(new Func1<Observable<byte[]>, Observable<byte[]>>() {
                @Override
                public Observable<byte[]> call(Observable<byte[]> observable) {
                    return observable;
                }
            })
            .subscribe(new Action1<byte[]>() {
                @Override
                public void call(byte[] data) {
                    if (BleHistoricalResponse.isHistoricalHeader(data)) {
                        currentResponse = new BleHistoricalResponse();
                        currentResponse.appendData(data);
                    } else if (BleHistoricalResponse.isHistoricalTrailer(data) && currentResponse != null) {
                        currentResponse.appendData(data);

                        bleHistoricalStreamObservable.onNext(currentResponse);
                        currentResponse = null;
                    } else if (currentResponse != null) {
                        currentResponse.appendData(data);
                    }
                }
            }, new Action1<Throwable>() {
                @Override
                public void call(Throwable throwable) {
                    throwable.printStackTrace();
                }
            });
}

StepsSyncManager

Finally the StepsSyncManager is responsible for connecting the BleManager and FitManager together, by retrieving steps data from our BleManager and pushing them to the FitManager. The StepsSyncManager is started through a sticky service and kept running in the background all the time. This insures steps data are kept up to data with the Google Fit API, and if the users were to open the Google Fit App at any moment they would be represented with an accurate count of their steps for the day.

Steps syncing is divided into two parts: the first is retrieving the steps data from our BLE device and the second pushing that data to the Google Fit API.

Retrieving steps data from BLE

To regularly receive step data updates from our BLE device, we had to setup a new BLE notification on the characteristic responsible for publishing steps data updates.

Once we were subscribed to the steps data notifications we had to slow down the amount of updates we were receiving, as our BLE device pushes multiple updates a second and we wanted to avoid hammering the Google Fit API with hundreds of requests every couple of minutes. To achieve that we used the RxJava Sample operator, which allowed us to only receive the last published update every 30 minutes.

public void startSyncing() {
    syncSubscription = bleManager.getStreamingDataObservable()
            .sample(30, TimeUnit.MINUTES)
            .startWith(bleManager.getStreamingDataObservable().take(1))
            .observeOn(ioSched)
            .subscribe(new Action1<BleResponse>() {
                @Override
                public void call(BleResponse response) {
                    saveDataToGoogleFit(response.getStepsResponse());
                }
            }, new Action1<Throwable>() {
                @Override
                public void call(Throwable throwable) {
                    ....
                }
            });
}

Pushing steps data to Google Fit API

After we’ve received new steps data update , we need to push these updates to the Google Fit API. As our steps data updates contain the total steps count for the day and we only need to push the number of the steps since our last update, we first need to retrieve the total steps count since our last update from the Google Fit API which then we can use to work out the steps count delta. Finally after we have worked out the amount of steps walked since our last update we can insert a new steps record using our FitManager as shown below.

private void saveDataToGoogleFit(final int stepsResponse) {
    fitManager.getDailyStepsCount()
            .flatMap(new Func1<DailyTotalResult, Observable<Long>>() {
                @Override
                public Observable<Long> call(DailyTotalResult dailyTotalResult) {
                    int delta = stepsResponse - getDailyStepsCount(dailyTotalResult);
                    return saveStepsDelta(delta);
                }
            })
            .subscribeOn(ioSched)
            .subscribe(new Action1<Long>() {
                @Override
                public void call(Long syncTime) {
                    logger.debug("Data saved: " + syncTime);
                    setLastSync(syncTime);
                }
            }, new Action1<Throwable>() {
                @Override
                public void call(Throwable throwable) {
                    logger.error("Error saving steps data", throwable);
                 }
            });
}

private Observable<Long> saveStepsDelta(int delta) {
    if (delta > 0) {
        long startTime = getLastUpdated();
        final long endTime = System.currentTimeMillis();

        return fitManager.insertNewSteps(startTime, endTime, delta)
                .flatMap(new Func1<Status, Observable<Long>>() {
                    @Override
                    public Observable<Long> call(Status status) {
                        if (status.isSuccess()) {
                            return Observable.just(endTime);
                        }

                        return Observable.error(new Exception(status.getStatusMessage()));
                    }
                });
    }

    return Observable.empty();
}

Conclusion

The Google Fit API provides great value at a very low cost for both the developer and user. It removes all the complications that come with implementing your own backend to store all the steps data, while providing the user with a tremendous set of tools to interact and better analyze the collected steps data using the Google Fit App.

Related articles: