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



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

    DataArt
    118,92
    Технологический консалтинг и разработка ПО
    Поделиться публикацией

    Комментарии 10

      +1
      Все красиво, лишь о разработке под ios ни слова не нашел. Хотя почти до конца всю статью прочитал.
        +7
        Было бы замечательно сделать скриншоты андроида раза в два (а лучше в три) поменьше.
        Когда скрин смартфона не помещается на мой экран десктопа — это не очень удобно: )
          +1
          До сих пор удивляюсь почему разрешение моего монитора меньше экрана смартфона. Кто может пофиксить?
            0
            Картинки исправлены, уменьшены втрое.
          +1
          Забавно видеть GoogleFit и толстуху на картинке :)
            +1
            Она до гугла была заметно стройнее.
              0
              толстухам тоже нужно заниматься этими всеми вещами
              0
              Не знаю, на что это гуглевцами рассчитано, но их программа ведёт у меня просто отвратительно, показывает полнейший бред — в отличие от других спортивных трекеров.
                0
                Почему Google Fit недоступен в некоторых странах?
                  0
                  Не получается по инструкции запустить GogoleFitResearch. После выбора аккаунта "Неизвестная ошибка с Сервисами Google Play". Проверял на двух разных телефонах. В чем может быть проблема? Приложение подписал ключём, SHA1 которого добавил в свой проект в Google Developer Console. Может быть что то еще нужно сделать?

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое