Привет! Современные гаджеты и носимая электроника позволяют не только выходить в интернет откуда душе угодно, шарить и лайкать контент, но и следить за здоровьем, учитывать спортивные достижения и просто вести здоровый образ жизни.
Сегодня мы расскажем про основные возможности 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.
Подготовка проекта
- Для начала понадобится войти в свой Google-аккаунт (если по каким-то невероятным причинам у вас до сих пор его нет, исправить это недоразумение можно по следующей ссылке: https://accounts.google.com/SignUp);
- Залогинились? Переходим в Google Developer Console и создаем новый проект. Главное — не забыть включить для него Fitness API;
- Теперь необходимо добавить SHA1-ключ из проекта в консоль. Для этого используем утилиту keytool. Как это сделать, отлично описано в туториале по Google Fit. Обновляем Play Services до последней версии: они нужны для работы API, в первую очередь — для доступа к облачному хранилищу данных.
- Добавляем в 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 для взаимодействия с датчиком считывания пульса в умных часах
Удачи вам и успехов в спорте!
Автор: Developers_Relations