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

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

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

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

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

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

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

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

82025ca2e1949397cf77271b18abfbb7.png

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 устройствами. 7b97c5d294054744419de7ebfe856aee.png

Сам по себе Google Fitness API состоит из следующих модулей:

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

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

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

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

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

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

Добавляем в 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): Авторизация осуществляется с помощью стандартной веб-формы:

22220005b5ee5c73ac67f2697095b2ec.png

Результат исправления ошибки авторизации, которая была начата вызовом 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 обеспечивают получение живых данных с датчиков по заданному интервалу времени или событию.Для демонстрации работы отдельных 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() { @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 запрашивает список доступных источников данных (которые мы отображаем во фрагменте 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 + »]»); Полученный нами список источников данных может выглядеть так: 085ea1437bff5511acc8aa1272e3f12a.png

В нашем примере мы упростили задачу и подписываемся на обновления от каждого источника, подходящего под наши критерии. В реальной жизни есть смысл выбирать один источник, сужая критерии, чтобы не получать избыточные данные, засоряющие трафик. Подписываясь на сообщения от источника данных, мы можем задать также интервал считывания данных (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).7cadd968364ee484699a478a2fb14b1f.pngЗаписи не дают визуальных результатов, но их работу можно проследить через History API в виде сохраненных в облаке данных. Собственно, все, что можно сделать с помощью Recording API, — подписаться на события (чтобы система автоматически вела записи за нас, отписаться от них и произвести поиск существующих подписок): Скрытый текст 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.»); } } }); Здесь мы подписываемся на DataType.TYPE_STEP_COUNT_DELTA. При желании собирать данные других типов достаточно повторить вызов для другого типа данных.Получение списка существующих подписок выполняется так:

Скрытый текст 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 ()); } } }); Выглядят логи вкладки Recordings таким образом: ad931d5b986752d0eb9b834f72da0165.png

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() { @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); } } }

} }); В зависимости от типа запроса мы можем получить либо 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, с помощью чего считает, например, количество шагов и время активности за день).d1213a9afdca07de086aca0584b6fbf7.png

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

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

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

9f070a0425e4b2adda6c5e1b87081d93.png4d80f29c56d1f37ff6491668f3005a92.png4987550f5bd278e322df178615725dae.png

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

bcfcf1781c94a9561a2b90a32a4505fc.png

© Habrahabr.ru