Blog How To: Use GoogleFit API

Picture1

Today we’ll talk about the basics of the GoogleFit API for the Android platform and will try to put this knowledge into practice. We’ll create a project and read fitness data from the available sensors, store them in a GoogleFit cloud and later read the history. Also we will overview the possibilities to apply this experience to real projects.

So what?

GoogleFit is a small and well-documented platform. You can read a lot of useful information on the Google Developers portal where a whole section about GoogleFit exists. But for those who prefer starting from a simple overview and not diving deeply in the documentation, the videos by Lisa Wray, the official Google Developer Advocate can be the perfect start.

For example, you can start with this tutorial:

You can also look at a sample project from the Google Developers on GitHub.

GoogleFit gives us the ability to get fitness data from different sources (sensors that are installed in smartphones, smartwatches, or fitness wristbands), save data to cloud storage and read a history of fitness measurements from sets of training sessions.

To access data you can use the native Android API as well as the REST API for web client development.

The most important role in the GoogleFit ecosystem comes from wearable gadgets. Besides “classical” smartwatches, the system supports data from fitness wristbands such as Nike+ and Jawbone Up and also standalone Bluetooth sensors. As we said before, data is stored in a cloud, so we can get statistics and freely combine information from different sources.

Picture2

The Fit API is a part of Google Play Services. As you probably already know, having the most recent update of Play Services may be even more important than having the latest version of Android OS. Due to such distribution of functionality between Services and OS, users of different generations of Android can enjoy the newest features that Google provides. The same is with the Google Fit API – it’s available for anyone who uses Android starting with Gingerbread (2.3, API level 9).

Before we start, let’s make some definitions about the Fit API:

  • Data Sources — usually these are hardware sensors. But they also can be software, for example, aggregating data from multiple other sensors.
  • Data Types — can be speed, steps count, heart rate or something similar. Data type can contain one (speed, steps) or multiple (location) fields.
  • Data Points — marks of fitness measurements that contain both measured value and time when measurement was done.
  • Datasets — collections of data points that are generated by some data source. Usually we will use them to retrieve data from a response to a query.
  • Sessions — groups of user activities such as running track, gym training, etc. Each session can contain some segments.
  • GATT (Generic Attribute Profile) — the protocol of communication between Bluetooth Low Energy devices.

Picture3

 

The Google Fitness API is structured into modules such as:

  • Sensors API — access to sensors and reading live data from them.
  • Recording API — automatically writes data to a cloud using a subscription mechanism.
  • History API — makes batch operations of reading, inserting, importing, and deleting data from a cloud.
  • Sessions API — provides the ability to store fitness data as sessions with segments.
  • Bluetooth Low Energy API — ensures access to Bluetooth Low Energy sensors in GoogleFit. Using this API we can discover available BLE sensors and connect to them in order to use them with other APIs (such as Recording and History).

GoogleFitResearch demo

We created a special project to show you basics of GoogleFit. You can get the source code from BitBucket.

We will start by reading live sensors data and use the Sensors API. It gives us the ability to obtain a list of available sensors, and we can choose from them. In our project we’ll try to obtain heart rate, steps count, and location of user.

Get started

To start we need a Google account. We don’t need to do anything ourselves, neither create a database nor set up a web server the GoogleFit API will do these jobs for us.

Project setup

  1. We need to log in with our Google account (you can obtain one here: https://accounts.google.com/SignUp);
  2. Then in the Google Developer Console we create a project and enable the Fitness API among other APIs available to our GoogleApiClient;

Picture4

  1. We also need to add the SHA1 key of the project to the Developer Console, using keytool. Detailed instructions can be obtained here: https://developers.google.com/fit/android/get-started#step_3_enable_the_fitness_api
  1. To be sure that everything goes right, it’s good to update the Play Services SDK to the last version:

Picture5

  1. Then go to build.gradle and add dependecy to Play Services:
dependencies {
    compile 'com.google.android.gms:play-services:6.5.+'
}

Authorization

Using the GoogleApiClient we connect to services. The code below creates a client that should ask Fitness.API for read permissions and add permissions for reading (SCOPE_LOCATION_READ) and writing (SCOPE_BODY_READ_WRITE), and also sets the Listeners that should obtain results and errors from Fitness.API:

client = new GoogleApiClient.Builder(activity)
                .addApi(Fitness.API)
                .addScope(Fitness.SCOPE_LOCATION_READ)
                .addScope(Fitness.SCOPE_ACTIVITY_READ)
                .addScope(Fitness.SCOPE_BODY_READ_WRITE)
                .addConnectionCallbacks(
                        new GoogleApiClient.ConnectionCallbacks() {
 
                            @Override
                            public void onConnected(Bundle bundle) {
                                display.show("Connected");
                                connection.onConnected();
                            }
 
                            @Override
                            public void onConnectionSuspended(int i) {
                                display.show("Connection suspended");
                                if (i == GoogleApiClient.ConnectionCallbacks.CAUSE_NETWORK_LOST) {
                                    display.show("Connection lost. Cause: Network Lost.");
                                } else if (i == GoogleApiClient.ConnectionCallbacks.CAUSE_SERVICE_DISCONNECTED) {
                                    display.show("Connection lost. Reason: Service Disconnected");
                                }
                            }
                        }
                )
                .addOnConnectionFailedListener(
                        new GoogleApiClient.OnConnectionFailedListener() {
                            // Called whenever the API client fails to connect.
                            @Override
                            public void onConnectionFailed(ConnectionResult result) {
                                display.log("Connection failed. Cause: " + result.toString());
                                if (!result.hasResolution()) {
                                    GooglePlayServicesUtil.getErrorDialog(result.getErrorCode(), activity, 0).show();
                                    return;
                                }
 
                                if (!authInProgress) {
                                    try {
                                        display.show("Attempting to resolve failed connection");
                                        authInProgress = true;
                                        result.startResolutionForResult(activity, REQUEST_OAUTH);
                                    } catch (IntentSender.SendIntentException e) {
                                        display.show("Exception while starting resolution activity: " + e.getMessage());
                                    }
                                }
                            }
                        }
                )
                .build();
 
 
        сlient.connect();

GoogleApiClient.ConnectionCallbacks will inform us about successful (onConnected) or unsuccessful (onConnectionSuspended) connection. GoogleApiClient.OnConnectionFailedListener provides us with a way to handle connection errors – e.g.: the absence of authorization on the first call to the GoogleFit API, so a user can accept or decline permissions for our application’s requests. This is performed through the usual OAuth web form with a call to result.startResolutionForResult:

Picture6

Resolution of authorization, started with a call to startResolutionForResult should be handled in onActivityResult:

@Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_OAUTH) {
            display.log("onActivityResult: REQUEST_OAUTH");
            authInProgress = false;
            if (resultCode == Activity.RESULT_OK) {
                // Make sure the app is not already connected or attempting to connect
                if (!client.isConnecting() && !client.isConnected()) {
                    display.log("onActivityResult: client.connect()");
                    client.connect();
                }
            }
        }
    }

We use variable authInProgress to exclude starting authorization twice. After successful authorization we connect client calling mClient.connect(). It’s placed in code twice – first time we tried to call it from onStart() but it caused fail because lack of authorization and now we granted permissions and call it once more because are sure in result.

Sensors API

The Sensors API gives us a live stream of data from sensors.

To demonstrate the work of separate APIs in the research project we added custom wrappers that only the most important common calls are left in MainActivity. For example in the SensorsAPI  onConnected() event we call:

display.show("client connected");
//                we can call specific api only after GoogleApiClient connection succeeded
                        initSensors();
                        display.show("list datasources");
                        sensors.listDatasources();

But inside that we perform the true work with the Sensors API:

Fitness.SensorsApi.findDataSources(client, new DataSourcesRequest.Builder()
                .setDataTypes(
                        DataType.TYPE_LOCATION_SAMPLE,
                        DataType.TYPE_STEP_COUNT_DELTA,
                        DataType.TYPE_DISTANCE_DELTA,
                        DataType.TYPE_HEART_RATE_BPM )
                .setDataSourceTypes(DataSource.TYPE_RAW, DataSource.TYPE_DERIVED)
                .build())
                .setResultCallback(new ResultCallback() {
                    @Override
                    public void onResult(DataSourcesResult dataSourcesResult) {
 
                        datasources.clear();
                        for (DataSource dataSource : dataSourcesResult.getDataSources()) {
                            Device device = dataSource.getDevice();
                            String fields = dataSource.getDataType().getFields().toString();
                            datasources.add(device.getManufacturer() + " " + device.getModel() + " [" + dataSource.getDataType().getName() + " " + fields + "]");
 
                            final DataType dataType = dataSource.getDataType();
                            if (    dataType.equals(DataType.TYPE_LOCATION_SAMPLE) ||
                                    dataType.equals(DataType.TYPE_STEP_COUNT_DELTA) ||
                                    dataType.equals(DataType.TYPE_DISTANCE_DELTA) ||
                                    dataType.equals(DataType.TYPE_HEART_RATE_BPM)) {
 
                                Fitness.SensorsApi.add(client,
                                        new SensorRequest.Builder()
                                                .setDataSource(dataSource)
                                                .setDataType(dataSource.getDataType())
                                                .setSamplingRate(5, TimeUnit.SECONDS)
                                                .build(),
                                        new OnDataPointListener() {
                                            @Override
                                            public void onDataPoint(DataPoint dataPoint) {
                                                String msg = "onDataPoint: ";
                                                for (Field field : dataPoint.getDataType().getFields()) {
                                                    Value value = dataPoint.getValue(field);
                                                    msg += "onDataPoint: " + field + "=" + value + ", ";
                                                }
                                                display.show(msg);
                                            }
                                        })
                                        .setResultCallback(new ResultCallback() {
                                            @Override
                                            public void onResult(Status status) {
                                                if (status.isSuccess()) {
                                                    display.show("Listener for " + dataType.getName() + " registered");
                                                } else {
                                                    display.show("Failed to register listener for " + dataType.getName());
                                                }
                                            }
                                        });
                            }
                        }
                        datasourcesListener.onDatasourcesListed();
                    }
                });

Fitness.SensorsApi.findDataSources requests the list of available datasources and we display them in the Datasources fragment in the UI.

DataSourcesRequest need to include filters for DataTypes, for example: TYPE_STEP_COUNT_DELTA.

As a result of a request we obtain DataSourcesResult which contains details of datasources (device name, vendor, data types, fields):

 for (DataSource dataSource : dataSourcesResult.getDataSources()) {
                            Device device = dataSource.getDevice();
                            String fields = dataSource.getDataType().getFields().toString();
                            datasources.add(device.getManufacturer() + " " + device.getModel() + " [" + dataSource.getDataType().getName() + " " + fields + "]");

In the application we display the obtained list of datasources in a ListView:

Picture7

In our research project we simplified the task and made subscriptions for all available sources from responses. In a real project there should be a reason to choose a narrow criteria, obtaining only the data we really need. When making a subscription we can set the interval of the readings (SamplingRate):

Fitness.SensorsApi.add(client,
                                        new SensorRequest.Builder()
                                                .setDataSource(dataSource)
                                                .setDataType(dataSource.getDataType())
                                                .setSamplingRate(5, TimeUnit.SECONDS)
                                                .build(),
                                        new OnDataPointListener() {}

DataPoint contains measurements from sensor. Evidently, the sensor data variations range is huge, so to describe them GoogleFit uses a variable structure described through a “fields” array, so each value correspond to its field:

new OnDataPointListener() {
                                            @Override
                                            public void onDataPoint(DataPoint dataPoint) {
                                                String msg = "onDataPoint: ";
                                                for (Field field : dataPoint.getDataType().getFields()) {
                                                    Value value = dataPoint.getValue(field);
                                                    msg += "onDataPoint: " + field + "=" + value + ", ";
                                                }
                                                display.show(msg);
                                            }
                                        })

For example, a step counter (delta) produces a new record with each step (to be more exact, on a batch of movements that the sensor considers to count as a step). For example, during tests I emulated steps by simple shaking my Nexus 5. :)

Picture8

With AndroidWear things become simpler – there is no need for an authorization screen (OAuth). GoogleFit starts on the smartwatch with mobile authorization and gives us the same access to sensors as on the mobile device.

The paired watch heart rate sensor isn’t listed in the sensors listing on my Nexus 5, so I added the wearable module to the research project. There we set the filter to DataType.TYPE_HEART_RATE_BPM to obtain just this data. The results look like:

Picture9Picture10

Under the smartwatch we can see a bright green light – it’s the PPG (Heart rate monitor) and it is woken by request from the GoogleFit API and now tries to read the heartrate.

Picture11

Recording API

The results of the recordings aren’t as visible, but their output can be seen through the History API as data saved to the cloud. All the Recording API does is just subscribing GoogleFit for the exact type of data to make automatic recordings.

Fitness.RecordingApi.subscribe(client, DataType.TYPE_STEP_COUNT_DELTA)
                .setResultCallback(new ResultCallback() {
                    @Override
                    public void onResult(Status status) {
                        if (status.isSuccess()) {
                            if (status.getStatusCode() == FitnessStatusCodes.SUCCESS_ALREADY_SUBSCRIBED) {
                                display.show("Existing subscription for activity detected.");
                            } else {
                                display.show("Successfully subscribed!");
                            }
                        } else {
                            display.show("There was a problem subscribing.");
                        }
                    }
                });

In the code above we subscribe to DataType.TYPE_STEP_COUNT_DELTA. For subscription to another data type it is enough just to call this one more time.

To obtain list of currently active subscriptions we do:

Fitness.RecordingApi.listSubscriptions(client, DataType.TYPE_STEP_COUNT_DELTA).setResultCallback(new ResultCallback() {
                    @Override
                    public void onResult(ListSubscriptionsResult listSubscriptionsResult) {
                        for (Subscription sc : listSubscriptionsResult.getSubscriptions()) {
                            DataType dt = sc.getDataType();
                            display.show("found subscription for data type: " + dt.getName());
                        }
                    }
                });

Logs from the Recordings tab looks like:

Picture12

History API

The History API gives us access to buckets of data from the cloud. Here we can read data over some specified ranges of time (batch history data), delete old records, and more.

DataReadRequest readRequest = new DataReadRequest.Builder()
                .aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA)
                .bucketByTime(1, TimeUnit.DAYS)
                .setTimeRange(start, end, TimeUnit.MILLISECONDS)
                .build();

When we create request (DataReadRequest) we can set aggregation rules, for example, TYPE_STEP_COUNT_DELTA в AGGREGATE_STEP_COUNT_DELTA, obtaining a summary of steps, set the sampling rate (.bucketByTime), or the time range of history (.setTimeRange)

Fitness.HistoryApi.readData(client, readRequest).setResultCallback(new ResultCallback() {
            @Override
            public void onResult(DataReadResult dataReadResult) {
                if (dataReadResult.getBuckets().size() > 0) {
                    display.show("DataSet.size(): "
                            + dataReadResult.getBuckets().size());
                    for (Bucket bucket : dataReadResult.getBuckets()) {
                        List dataSets = bucket.getDataSets();
                        for (DataSet dataSet : dataSets) {
                            display.show("dataSet.dataType: " + dataSet.getDataType().getName());
 
                            for (DataPoint dp : dataSet.getDataPoints()) {
                                describeDataPoint(dp, dateFormat);
                            }
                        }
                    }
                } else if (dataReadResult.getDataSets().size() > 0) {
                    display.show("dataSet.size(): " + dataReadResult.getDataSets().size());
                    for (DataSet dataSet : dataReadResult.getDataSets()) {
                        display.show("dataType: " + dataSet.getDataType().getName());
 
                        for (DataPoint dp : dataSet.getDataPoints()) {
                            describeDataPoint(dp, dateFormat);
                        }
                    }
                }
 
            }
        });

In the response we can obtain buckets with dataReadResult.getBuckets() or datasets dataReadResult.getDataSets().

The bucket is just a collection of datasets, but the API gives us choice, if there is only one bucket, we can access datasets without the additional step of iterating through the buckets.

The code to read datapoints looks like:

public void describeDataPoint(DataPoint dp, DateFormat dateFormat) {
        String msg = "dataPoint: "
                + "type: " + dp.getDataType().getName() +"\n"
                + ", range: [" + dateFormat.format(dp.getStartTime(TimeUnit.MILLISECONDS)) + "-" + dateFormat.format(dp.getEndTime(TimeUnit.MILLISECONDS)) + "]\n"
                + ", fields: [";
 
        for(Field field : dp.getDataType().getFields()) {
            msg += field.getName() + "=" + dp.getValue(field) + " ";
        }
 
        msg += "]";
        display.show(msg);
    }

We should obtain logs with information from previous sessions, recorded through the Recording API and also with the data from the official GoogleFit application (it also activates the Recording API and stores to the cloud the step count and activities for the day).

Picture13

What’s next?

So, we briefly looked at the abilities to read data from sensors (Sensors API), record data automatically (Recording API), and work with the history of recordings (History API). These parts provide the basic functionality of a fitness application, enough to build a classical fitness tracker app.
But of course there is more.

There are two additional APIs: Sessions and Bluetooth. The first gives us the ability to group activities into sessions and set segments in order to make sessions more structured (https://developers.google.com/fit/android/using-sessions). The second gives us the ability to discover and connect Bluetooth sensors that are in the range of bluetooth activity, such as cardio monitors or smart wear sensors in clothes, caps, or shoes (https://developers.google.com/fit/android/ble-sensors). GoogleFit also gives us the ability to create sensors programmatically and aggregate data from multiple sensors using custom rules or to connect devices that have not implemented the necessary protocols (this can be implemented with FitnessSensorService).

And of course if you want to make a real application with the GoogleFit API, you probably should add two more things: some chart building tools to display statistics in the way the official GoogleFit apps do (one such libraries is https://bitbucket.org/danielnadeau/holographlibrary/wiki/Home) and  a wearable platform oriented to working with fitness data, AndroidWear (https://developer.android.com/training/building-wearables.html).

Picture14Picture15Picture16

Good luck and perfect Fit to you!

Please subscribe to our newsletters below to follow our future updates!


Subscribe to our news


One thought on “How To: Use GoogleFit API”

  1. Purnachandra Patro ( )

    Great post. Very well researched. I have been studying Fit API for around a week now. But this comprehensive post has answered my all doubts.

Leave a Reply