GoogleFit API - стартуємо і бачимо результат

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



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

Спасибі ConstantineMars за допомогу в підготовці статті.

Що до чого
GoogleFit — досить невелика і добре документована платформа. Необхідну для роботи з нею інформацію можна подивитися на нашому порталі Google Developers, там взаємодії з Fit присвячений цілий розділ. Для тих же, кому не хочеться з головою поринати в опису 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 client API 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 connection failed");
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 для взаємодії з датчиком зчитування пульсу в розумних годинах



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



Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.