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

от автора

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

Сегодня мы расскажем про основные возможности 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 для взаимодействия с датчиком считывания пульса в умных часах

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

ссылка на оригинал статьи http://habrahabr.ru/post/252083/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *