GoogleFit API - we start and see the result

    Hi, Habrahabr! Modern gadgets and wearable electronics allow you not only to go online from anywhere you want, to fumble and like content, but also to monitor your health, take into account sports achievements and just lead a healthy lifestyle.



    Today we will talk about the main features of the GoogleFit API on the Android platform and try to put the information into practice: learn to read data from sensors available in the system, save it to the cloud and read the history of records. We will also create a project that implements these tasks, and consider the general prospects for using the GoogleFit API in real-world developments.

    Thanks to ConstantineMars for helping with this article.

    What is what


    GoogleFit is a fairly small and well-documented platform. Information necessary for working with it can be viewed on our Google Developers portal, where a whole section is devoted to interaction with Fit . For those who do not want to dive headlong into the API descriptions, but are interested in learning about the main features of the platform in order, a great start is the video of Lisa Wray, the official Google Developer Advocate.

    You can start exploring the Fit platform from this tutorial:


    GoogleFit allows you to receive fitness data from various sources (sensors installed in phones, smart watches, fitness bracelets), save them to the cloud storage and read them in the form of a history of “fitness measurements” or a set of sessions / workouts.

    To access the data, you can use both native APIs for Android and the REST API for writing a web client.

    A major role in the GoogleFit ecosystem is played by wearable gadgets, which are highly valued. In addition to the “classic” smart watches, the system supports data from specialized Nike + and Jawbone Up fitness bracelets or Bluetooth sensors. As we already said, data is stored in the cloud and allows you to view statistics, freely combining information from different sources.



    Fit API - part of Google Play Services. As many of you already know, it’s not as important to have the latest version of Android OS on your device as the updated Play Services. Due to the removal of such APIs to the part updated by Google, and not by smartphone manufacturers, users of your applications around the world can use completely different generations of systems. In particular, the Fit API is available to everyone who has Android version 2.3 or higher on the smartphone (Gingerbread, API level 9).

    To avoid unnecessary questions, let's outline the key concepts of Fit API:
    • Data Sources - data sources, i.e. sensors. They can be either hardware or software (created artificially, for example, by aggregating indicators of several hardware sensors).
    • Data Types - data types: speed, number of steps or pulse. A data type can be complex, containing several fields, such as location {latitude, longitude, and accuracy}.
    • Data Points - fitness measurement marks containing data binding to the measurement time.
    • Datasets - sets of points (data points) that belong to a specific data source (sensor). Sets are used to work with the data warehouse, in particular, to receive data in response to requests.
    • Sessions - Sessions that group user activity into logical units, such as a run or training. A session can contain several segments (Segment).
    • GATT (Generic Attribute Profile) is a protocol that provides structured data exchange between BLE devices .




    The Google Fitness API itself consists of the following modules:
    • Sensors API - provides access to sensors and reading live data stream from them.
    • Recording API - is responsible for the automatic recording of data in the repository using the mechanism of "subscriptions".
    • History API - provides group read, insert, import and delete operations in Google Fit.
    • Sessions API - allows you to save fitness data in the form of sessions and segments.
    • Bluetooth Low Energy API - Provides access to Google Low Energy Bluetooth sensors in GoogleFit. Using this API, we can find available BLE devices and receive data from them for storage in the cloud.


    Google FitResearch demo


    To demonstrate the capabilities of GoogleFit, we created a special project that will allow you to work with the API without bothering to write some basis on which everything will work. The source code for the GoogleFit Research demo can be taken from BitBucket .

    Let's start with the simplest: let's try to get data from sensors live by using the Sensors API for this.

    First of all, you need to decide which sensors we will take the source data from. The Sensors API provides a special method for this, which allows you to get a list of available sources of information, and we can choose one, several, or even all sensors from this list.

    As an example, we will try to read the heart rate indicators, the number of steps and the change in the user's coordinates. It should be noted that, although we turn to the heart rate monitor, we still do not receive data from it: the heart rate meter is available in smart watches and fitness trackers, but not in the smartphone itself, we agree that at the time of writing the code, there are no watches or sensors we don’t have a pulse - as there is no data from them either. So we can evaluate how the system responds to the “negative test”, i.e. the case when instead of the expected data we get at best - zeros, and at worst - an error message from the system.

    Up to all night to get started


    All you need to work with the example is your Google account. We don’t need to create a database or write our own server - the GoogleFit API has already taken care of everything.

    As an official example, you can use the sources from Google Developers, available on GitHub .

    Project preparation


    1. First you’ll need to log in to your Google account (if for some incredible reason you still don’t have one, you can fix this misunderstanding at the following link: https://accounts.google.com/SignUp );
    2. Logged in? Go to the Google Developer Console and create a new project. The main thing is to remember to enable the Fitness API for it;




    1. Now you need to add the SHA1 key from the project to the console. To do this, use the keytool utility. How to do this is well described in the Google Fit tutorial . We are updating Play Services to the latest version: they are needed for the API to work, first of all, for access to the cloud data storage.




    1. Add Play Services dependency to build.gradle of the project:


    dependencies {
    compile 'com.google.android.gms: play-services: 6.5. +'
    }


    Login


    We have more or less sorted out the preparation of the project, now we will go directly to the authorization code.

    We will connect to the services using GoogleApiClient. The following code creates a client object that requests Fitness.API from services, adds us read and write permissions (SCOPE_LOCATION_READ) and write permissions (SCOPE_BODY_READ_WRITE), and sets up Listeners that will process data and errors from Fitness.API. After that, this code fragment tries to connect to Google Play Services with the specified settings:
    Hidden text
    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() {
                                @OverridepublicvoidonConnected(Bundle bundle){
                                    display.show("Connected");
                                    connection.onConnected();
                                }
                                @OverridepublicvoidonConnectionSuspended(int i){
                                    display.show("Connection suspended");
                                    if (i == GoogleApiClient.ConnectionCallbacks.CAUSE_NETWORK_LOST) {
                                        display.show("Connection lost. Cause: Network Lost.");
                                    } elseif (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.@OverridepublicvoidonConnectionFailed(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 - provides the processing of successful (onConnected) or failed (onConnectionSuspended) connections.
    GoogleApiClient.OnConnectionFailedListener - handles connection errors and the most important situation - an authorization error the first time you access the GoogleFit API, thus giving the user an OAuth authorization web form (result.startResolutionForResult):

    Authorization is performed using the standard web form:



    The result of fixing the authorization error that was started by calling startResolutionForResult is processed in onActivityResult:
    Hidden text
    @OverridepublicvoidonActivityResult(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 connectif (!client.isConnecting() && !client.isConnected()) {
                        display.log("onActivityResult: client.connect()");
                        client.connect();
                    }
                }
            }
        }
    

    We use the authInProgress variable to prevent the authorization procedure from re-starting and the request ID REQUEST_OAUTH. If the result is successful, we connect the client by calling mClient.connect (). This is the challenge that we have already tried to implement in onCreate, and which we encountered an error with the very first authorization.

    Sensors API


    Sensors APIs provide live data from sensors for a specified time interval or event.

    To demonstrate the operation of individual APIs in our example, we added wrappers that leave only generalized code for calling from MainActivity. For example, for SensorsAPI in the onConnected () client callback, we call:
    Hidden text
    display.show("client connected");
    //                we can call specific api only after GoogleApiClient connection succeeded
                            initSensors();
                            display.show("list datasources");
                            sensors.listDatasources();
    

    Inside, the work with the Sensors API is hidden directly:
    Hidden text
    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<DataSourcesResult>() {
                        @OverridepublicvoidonResult(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() {
                                                @OverridepublicvoidonDataPoint(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<Status>() {
                                                @OverridepublicvoidonResult(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 a list of available data sources (which we display in the Datasources fragment).

    DataSourcesRequest should include the type filters for which we want to get sources, for example DataType.TYPE_STEP_COUNT_DELTA .

    As a result of the request, we get a DataSourcesResult, from which you can get the details of each data source (device, brand, data type, data type fields):
    Hidden text
    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 + "]");
    

    Our list of data sources may look like this:



    In our example, we simplified the task and subscribe to updates from each source that matches our criteria. In real life, it makes sense to choose a single source, narrowing down the criteria so as not to receive redundant data clogging the traffic. By subscribing to messages from a data source, we can also set the interval for reading data (SamplingRate):
    Hidden text
    Fitness.SensorsApi.add(client,
                                            new SensorRequest.Builder()
                                                    .setDataSource(dataSource)
                                                    .setDataType(dataSource.getDataType())
                                                    .setSamplingRate(5, TimeUnit.SECONDS)
                                                    .build(),
                                            new OnDataPointListener() { … }
    

    DataPoint - sensor readings. Naturally, the sensors are different, and their description is the so-called "fields", which we can read from the data type, together with the values:
    Hidden text
    new OnDataPointListener() {
                                                @OverridepublicvoidonDataPoint(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) gives us a new record for each step (or rather, what the sensor perceives as a step, because in this case we managed to do with the usual shaking of the phone to generate new records :-p).



    Recording API


    Records do not give visual results, but their work can be tracked through the History API in the form of data stored in the cloud. Actually, all that can be done using the Recording API is to subscribe to events (so that the system automatically records for us, unsubscribe from them and search for existing subscriptions):
    Hidden text
    Fitness.RecordingApi.subscribe(client, DataType.TYPE_STEP_COUNT_DELTA)
                    .setResultCallback(new ResultCallback<Status>() {
                        @OverridepublicvoidonResult(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.");
                            }
                        }
                    });
    

    Here we subscribe to DataType.TYPE_STEP_COUNT_DELTA. If you want to collect data of other types, it is enough to repeat the call for another type of data.

    Obtaining a list of existing subscriptions is as follows:
    Hidden text
    Fitness.RecordingApi.listSubscriptions(client, DataType.TYPE_STEP_COUNT_DELTA).setResultCallback(new ResultCallback<ListSubscriptionsResult>() {
                        @OverridepublicvoidonResult(ListSubscriptionsResult listSubscriptionsResult){
                            for (Subscription sc : listSubscriptionsResult.getSubscriptions()) {
                                DataType dt = sc.getDataType();
                                display.show("found subscription for data type: " + dt.getName());
                            }
                        }
                    });
    


    The logs of the Recordings tab look like this:


    History API


    The History API provides data packages that can be saved and downloaded from the cloud. This includes reading data at specific intervals, saving previously read data (unlike the Recording API, this is just a data packet, not a live stream), deleting records made from the same application.
    Hidden text
    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 generating a query (DataReadRequest), we can specify aggregation operations, for example, combine TYPE_STEP_COUNT_DELTA in AGGREGATE_STEP_COUNT_DELTA, representing the total number of steps for a selected period of time; specify the sampling interval (.bucketByTime), specify the time interval for which we need data (.setTimeRange).
    Hidden text
    Fitness.HistoryApi.readData(client, readRequest).setResultCallback(new ResultCallback<DataReadResult>() {
                @OverridepublicvoidonResult(DataReadResult dataReadResult){
                    if (dataReadResult.getBuckets().size() > 0) {
                        display.show("DataSet.size(): "
                                + dataReadResult.getBuckets().size());
                        for (Bucket bucket : dataReadResult.getBuckets()) {
                            List<DataSet> dataSets = bucket.getDataSets();
                            for (DataSet dataSet : dataSets) {
                                display.show("dataSet.dataType: " + dataSet.getDataType().getName());
                                for (DataPoint dp : dataSet.getDataPoints()) {
                                    describeDataPoint(dp, dateFormat);
                                }
                            }
                        }
                    } elseif (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);
                            }
                        }
                    }
                }
            });
    

    Depending on the type of request, we can get either buckets dataReadResult.getBuckets () or DataSets dataReadResult.getDataSets ().
    In essence, bucket is just a collection of DataSets, and the API gives us a choice: if there are no buckets in the API response, we can directly work with the DataSets collection from dataResult.
    Subtracting DataPoints can be done, for example, like this:
    Hidden text
    publicvoiddescribeDataPoint(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);
        }
    

    Our logs will be filled with information from previous sessions recorded through Recording, and what the official GoogleFit has collected for us (it also activates the Recording API, using which it considers, for example, the number of steps and the time of activity per day).



    What's next?


    So, we examined the possibilities of reading data directly from sensors (Sensors API), automated recording of sensor indicators in GoogleFit (Recording API) and working with history (History API). This is the basic functionality of a fitness tracker, which is enough for a full-fledged application.

    Then there are two more interesting APIs provided by GoogleFit - Sessions and Bluetooth. The first makes it possible to group types of activity in sessions and segments for a more structured work with fitness data . The second allows you to search and connect to Bluetooth sensors within range, such as heart rate monitors, sensors in shoes / clothes, etc.

    You can also create software sensors and thus provide work with devices that do not implement the necessary protocols, but provide data (implemented using FitnessSensorService). These features are not required, but they add good opportunities to get your own data types (aggregated from data from other sensors or generated by software) and can be used if necessary.

    Of course, if you start working with the GoogleFit API, you will want to make the application beautiful and enjoyable to use. For this, you may need two more components: displaying graphs similar to what the official GoogleFit draws (for which there are many external libraries, for example, Bitbucket , and almost certainly AndroidWear, which, in particular, provides an API for interacting with a heart rate sensor in a smart watch.



    Good luck and success in sports!


    Also popular now: