Pull to refresh
0
DataArt
Технологический консалтинг и разработка ПО

GoogleFit API — стартуем и видим результат

Reading time13 min
Views27K
Привет, Хабрахабр! Современные гаджеты и носимая электроника позволяют не только выходить в интернет откуда душе угодно, шарить и лайкать контент, но и следить за здоровьем, учитывать спортивные достижения и просто вести здоровый образ жизни.



Сегодня мы расскажем про основные возможности GoogleFit API на платформе Android и попробуем применить информацию на практике: научимся считывать данные с доступных в системе датчиков, сохранять их в облако и вычитывать историю записей. Еще мы создадим проект, реализующий эти задачи, и рассмотрим общие перспективы применения GoogleFit API в реальных разработках.

Спасибо ConstantineMars за помощь в подготовке статьи.

Что к чему


GoogleFit — достаточно небольшая и хорошо документированная платформа. Необходимую для работы с ней информацию можно посмотреть на нашем портале Google Developers, там взаимодействию с Fit посвящён целый раздел. Для тех же, кому не хочется с головой нырять в опиcания API, а интересно узнать об основных возможностях платформы по порядку, отличным стартом послужит видео Lisa Wray, официального Google Developer Advocate.

Начать знакомство с платформой Fit можно с этого туториала:


GoogleFit позволяет получать фитнес-данные с различных источников (сенсоров, установленных в телефонах, умных часах, фитнес-браслетах), сохранять их в облачное хранилище и считывать в виде истории «фитнес-измерений» или набора сессий/тренировок.

Для доступа к данным можно использовать и нативные API под Android, и REST API для написания веб-клиента.

Важнейшую роль в экосистеме GoogleFit играют носимые гаджеты, на которые делаются большие ставки. Кроме «классических» умных часов, система поддерживает данные со специализированных фитнес-браслетов Nike+ и Jawbone Up или Bluetooth датчиков. Как мы уже говорили, данные сохраняются в облаке и позволяют просматривать статистику, свободно комбинируя информацию из разных источников.



Fit API — часть Google Play Services. Как многие из вас уже знают, не так важно иметь последнюю версию OS Android на вашем устройстве, как обновленные Play Services. Благодаря выносу подобных API в часть, обновляемую Google, а не производителями смартфонов, пользователи ваших приложений по всему миру могут использовать совершенно разные поколения систем. В частности, Fit API доступен всем, у кого на смартфоне стоит Android версии 2.3 или выше (Gingerbread, API level 9).

Чтобы не возникало лишних вопросов, давайте обозначим ключевые понятия Fit API:
  • Data Sources — источники данных, т. е. датчики. Они могут быть и аппаратными, и программными (созданными искусственно, например, путем агрегирования показателей нескольких аппаратных датчиков).
  • Data Types — типы данных: скорость, количество шагов или пульс. Тип данных может быть сложным, содержащим несколько полей, например, location {latitude, longitude, и accuracy}.
  • Data Points — отметки фитнес-замеров, содержащие привязку данных ко времени замера.
  • Datasets — наборы точек (data points), принадлежащих определенному источнику данных (датчику). Наборы используются для работы с хранилищем данных, в частности, для получения данных в ответ на запросы.
  • Sessions — сессии, которые группируют активность пользователя в логические единицы, такие как забег или тренировка. Сессия может содержать несколько сегментов (Segment).
  • GATT (Generic Attribute Profile) — протокол, обеспечивающий структурированный обмен данными между BLE устройствами.




Сам по себе Google Fitness API состоит из следующих модулей:
  • Sensors API — обеспечивает доступ к датчикам (sensors) и считывание живого потока данных с них.
  • Recording API — отвечает за автоматическую запись данных в хранилище, используя механизм «подписок».
  • History API — обеспечивает групповые операции считывания, вставки, импорта и удаления данных в Google Fit.
  • Sessions API — позволяет сохранять фитнес-данные в виде сессий и сегментов.
  • Bluetooth Low Energy API — обеспечивает доступ к датчикам Bluetooth Low Energy в GoogleFit. С помощью этого API мы можем находить доступные BLE девайсы и получать данные с них для хранения в облаке.


GoogleFitResearch demo


Для демонстрации возможностей GoogleFit мы создали специальный проект, который позволит вам поработать с API не утруждая себя написанием некоторого базиса, на котором все будет работать. Исходный код GoogleFit Research demo можно забрать на BitBucket.

Начнем с самого простого: попробуем получить данные с сенсоров вживую, применив для этого Sensors API.

Перво-наперво надо определиться, с каких датчиков будем забирать исходные данные. В Sensors API для этого предусмотрен специальный метод, который позволяет получить список доступных источников информации, а мы можем выбирать из этого списка один, несколько или хоть все датчики.

В качестве примера мы попробуем считать показатели частоты пульса, количество шагов и изменение координат пользователя. Надо отметить, что, хотя мы и обращаемся к пульсомеру, данных с него всё равно пока не получим: измеритель пульса доступен в умных часах и фитнес-трекерах, но не в самом смартфоне, условимся, что на момент написания кода ни часов, ни датчиков пульса у нас нет — как данных с них тоже нет. Так мы сможем оценить, как система реагирует на «негатвный тест», т.е. случай, когда вместо ожидаемых данных мы получаем в лучшем случае — нули, а в худшем — сообщение от системы об ошибке.

Up to all night to get started


Всё, что потребуется для работы с примером — ваш Google-аккаунт. Нам не понадобится ни создавать базу данных, ни писать собственный сервер — GoogleFit API уже позаботился обо всем.

В качестве официального примера можно использовать исходники от Google Developers, доступные на GitHub.

Подготовка проекта


  1. Для начала понадобится войти в свой Google-аккаунт (если по каким-то невероятным причинам у вас до сих пор его нет, исправить это недоразумение можно по следующей ссылке: https://accounts.google.com/SignUp);
  2. Залогинились? Переходим в Google Developer Console и создаем новый проект. Главное — не забыть включить для него Fitness API;




  1. Теперь необходимо добавить SHA1-ключ из проекта в консоль. Для этого используем утилиту keytool. Как это сделать, отлично описано в туториале по Google Fit. Обновляем Play Services до последней версии: они нужны для работы API, в первую очередь — для доступа к облачному хранилищу данных.




  1. Добавляем в build.gradle проекта зависимость от Play Services:


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


Авторизация


С подготовкой проекта более или менее разобрались, теперь перейдем непосредственно к коду авторизации.

Соединяться с сервисами будем при помощи GoogleApiClient. Следующий код создает объект клиента, который запрашивает Fitness.API у сервисов, добавляет нам права доступа на чтение (SCOPE_LOCATION_READ) и запись (SCOPE_BODY_READ_WRITE) и задает Listener’ы, которые будут обрабатывать данные и ошибки из Fitness.API. После этого данный фрагмент кода пробует подключиться к Google Play Services с заданными настройками:
Скрытый текст
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 — обеспечивает обработку удачного (onConnected) или неудачного (onConnectionSuspended) подключения.
GoogleApiClient.OnConnectionFailedListener — обрабатывает ошибки подключения и самую главную ситуацию — ошибку авторизации при первом обращении к GoogleFit API, таким образом выдавая пользователю веб-форму OAuth-авторизации (result.startResolutionForResult):

Авторизация осуществляется с помощью стандартной веб-формы:



Результат исправления ошибки авторизации, которая была начата вызовом startResolutionForResult, обрабатывается в 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();
                }
            }
        }
    }

Мы используем переменную authInProgress для исключения повтороного запуска процедуры авторизации и ID запроса REQUEST_OAUTH. При успешном результате подключаем клиент вызовом mClient.connect(). Это тот вызов, который мы уже пробовали осуществить в onCreate, и на который нам пришла ошибка при самой первой авторизации.

Sensors API


Sensors API обеспечивают получение живых данных с датчиков по заданному интервалу времени или событию.

Для демонстрации работы отдельных API в нашем примере мы добавили врапперы, которые оставляют для вызова из MainActivity только обобщенный код. Например, для SensorsAPI в onConnected() коллбэке клиента мы вызываем:
Скрытый текст
display.show("client connected");
//                we can call specific api only after GoogleApiClient connection succeeded
                        initSensors();
                        display.show("list datasources");
                        sensors.listDatasources();

Внутри же кроется непосредственно работа с 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<DataSourcesResult>() {
                    @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<Status>() {
                                            @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 запрашивает список доступных источников данных (которые мы отображаем во фрагменте Datasources).

DataSourcesRequest должен включать в себя фильтры типов, для которых мы хотим получить источники, например DataType.TYPE_STEP_COUNT_DELTA.

В результате запроса мы получаем DataSourcesResult, из которого можно получить детали каждого источника данных (устройство, бренд, тип данных, поля типа данных):
Скрытый текст
 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 + "]");

Полученный нами список источников данных может выглядеть так:



В нашем примере мы упростили задачу и подписываемся на обновления от каждого источника, подходящего под наши критерии. В реальной жизни есть смысл выбирать один источник, сужая критерии, чтобы не получать избыточные данные, засоряющие трафик. Подписываясь на сообщения от источника данных, мы можем задать также интервал считывания данных (SamplingRate):
Скрытый текст
Fitness.SensorsApi.add(client,
                                        new SensorRequest.Builder()
                                                .setDataSource(dataSource)
                                                .setDataType(dataSource.getDataType())
                                                .setSamplingRate(5, TimeUnit.SECONDS)
                                                .build(),
                                        new OnDataPointListener() { … }

DataPoint — показания датчика. Естественно, датчики бывают разные, и описанием их являются так называемые «поля» (fields), которые можем считать из типа данных, вместе со значениями:
Скрытый текст
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);
                                            }
                                        })

Например, счетчик шагов (delta) выдает нам новую запись на каждый шаг (вернее, на то, что датчик воспринимает как шаг, т.к. в данном случае удалось обойтись обычным потряхиванием телефоном для генерации новых записей :-p ).



Recording API


Записи не дают визуальных результатов, но их работу можно проследить через History API в виде сохраненных в облаке данных. Собственно, все, что можно сделать с помощью Recording API, — подписаться на события (чтобы система автоматически вела записи за нас, отписаться от них и произвести поиск существующих подписок):
Скрытый текст
Fitness.RecordingApi.subscribe(client, DataType.TYPE_STEP_COUNT_DELTA)
                .setResultCallback(new ResultCallback<Status>() {
                    @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.");
                        }
                    }
                });

Здесь мы подписываемся на DataType.TYPE_STEP_COUNT_DELTA. При желании собирать данные других типов достаточно повторить вызов для другого типа данных.

Получение списка существующих подписок выполняется так:
Скрытый текст
Fitness.RecordingApi.listSubscriptions(client, DataType.TYPE_STEP_COUNT_DELTA).setResultCallback(new ResultCallback<ListSubscriptionsResult>() {
                    @Override
                    public void onResult(ListSubscriptionsResult listSubscriptionsResult) {
                        for (Subscription sc : listSubscriptionsResult.getSubscriptions()) {
                            DataType dt = sc.getDataType();
                            display.show("found subscription for data type: " + dt.getName());
                        }
                    }
                });


Выглядят логи вкладки Recordings таким образом:


History API


History API обеспечивает работу с пакетами данных, которые можно сохранять и загружать из облака. Сюда входят считывание данных в определенных промежутках времени, сохранение ранее считанных данных (в отличие от Recording API это именно пакет данных, а не живой поток), удаление записей, сделанных из этого же приложения.
Скрытый текст
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();

При формировании запроса (DataReadRequest) мы можем задавать операции агрегирования, например, объединять TYPE_STEP_COUNT_DELTA в AGGREGATE_STEP_COUNT_DELTA, представляя суммарное количество шагов за выбранный промежуток времени; указывать промежуток сэмплирования (.bucketByTime), задавать интервал времени, для которого нам нужны данные (.setTimeRange).
Скрытый текст
Fitness.HistoryApi.readData(client, readRequest).setResultCallback(new ResultCallback<DataReadResult>() {
            @Override
            public void onResult(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);
                            }
                        }
                    }
                } 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);
                        }
                    }
                }

            }
        });

В зависимости от типа запроса мы можем получить либо buckets dataReadResult.getBuckets(), либо DataSets dataReadResult.getDataSets().
В сущности, bucket — просто коллекция DataSets, и API предоставляет нам выбор: если buckets в ответе API нет, мы можем напрямую работать с коллекцией DataSets из dataResult.
Вычитывание DataPoints можно выполнить, например, так:
Скрытый текст
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);
    }

Наши логи будут заполнены информацией из предыдущих сессий, записанных через Recording, и тем, что собрал для нас официальный GoogleFit (он тоже активирует Recording API, с помощью чего считает, например, количество шагов и время активности за день).



Что дальше?


Итак, мы рассмотрели возможности считывания данных непосредственно с датчиков (Sensors API), автоматизированной записи показателей датчиков в GoogleFit (Recording API) и работы с историей (History API). Это базовая функциональность фитнес-трекера, которого вполне достаточно для полноценного приложения.

Дальше есть еще два интересных API, предоставляемых GoogleFit — Sessions и Bluetooth. Первый дает возможность группировать виды активности в сессии и сегменты для более структурированной работы с фитнес-данными. Второй позволяет искать и подключаться к Bluetooth-датчикам, находящимся в радиусе досягаемости, таким как кардиомониторы, датчики в обуви/одежде и т. п.

Еще вы можете создавать программные сенсоры и таким образом обеспечивать работу с устройствами, которые не реализуют необходимые протоколы, но предоставляют данные (реализуется с помощью FitnessSensorService). Эти фичи не обязательны, но добавляют неплохие возможности для получения собственных типов данных (агрегированных из данных других датчиков или сгенерированных программно) и их можно использовать при необходимости.

Разумеется, если вы возьметесь работать с GoogleFit API, вам захочется сделать приложением красивым и приятным в использовании. Для этого могут понадобиться еще два компонента: отображение графиков, похожих на то, что рисует официальный GoogleFit (для чего есть множество внешних библиотек, например, на Bitbucket, и почти наверняка — AndroidWear, который, в частности, предоставляет API для взаимодействия с датчиком считывания пульса в умных часах



Удачи вам и успехов в спорте!

Tags:
Hubs:
Total votes 17: ↑15 and ↓2+13
Comments10

Articles

Information

Website
www.dataart.com
Registered
Founded
Employees
1,001–5,000 employees