Делаем шагомер для Android устройств на акселерометре

В этой статье мы делаем шагомер для Android на акселерометре на устройствах на которых нет датчика шагов. Когда мне нужно было сделать его быстро, достаточно точного способа это сделать на Android я не нашел, были только какие-то абстрактные статьи из которых были взяты методы обработки сырых данных с акселерометра. Так вот, в одной из них был подсмотрен способ, с помощью Быстрого преобразования Фурье и был реализован и отлажен в коде, могу сказать, что работает достаточно точно.

Верстаем интерфейс

Для ответа на вопрос как сделать шагомер для Android устройств на акселерометре, нужно начать с создания нового проекта в Android Studio. После этого у нас будет пустая Activity, в нее добавим TextView для отображения шагов.

Как сделать шагомер для Android устройств на акселерометре
Окно шагомера

В Activity при старте приложения мы создаем экземпляр StepRepositoryImpl и получаем на него ссылку, которая является членом класса, далее в onStart регистрируем BroadcastReceiver для получения уведомления о шаге для обновления UI, и когда пришло уведомление, мы из репозитория получаем сумму пройденных на сегодня шагов и отображаем в TextView, если Activity закрывают, отписываемся от уведомлений.

Сделаем простую верстку с одним TextView посередине:

Создаем Service для «постоянной» работы в фоновом режиме

Нет ничего более временного, чем постоянно работающий в фоне Service;) Ну а теперь серьезно, без хаков/костылей и прочей нечисти, сделать так, чтобы на Андроиде в фоне работал сервис практически невозможно(Может потребоваться больше времени), тем более на версиях начиная с Android 6.0 Marshmallow, там появился Doze, который валит все подряд ради экономии батареи.
А теперь перенесемся к нашему сервису шагомера:

Теперь добавим инфу о сервисе в манифест:

Давайте теперь разберемся как работает наш сервис:
Как было сказано выше, сервис запускается в MainActivity при ее старте:

После вызова startService происходит вызов onCreate() в сервисе, в котором мы запрашиваем SensorManager и проверяем наличие датчика шагомера hasSystemFeature(FEATURE_SENSOR_STEP_DETECTOR), если есть, тогда инстанцируем класс StepCounterManagerStepCounterSensor отвечающий за работу с этим датчиком на тех устройствах, на которых он есть. Если датчика шагомера нет, проверяем на наличие акселерометра hasSystemFeature(FEATURE_SENSOR_ACCELEROMETER), если есть, уже идет другой класс StepCounterManagerAccelerometer, А если вообще этих датчиков нет, кидаем исключение и останавливаем сервис. Когда мы инстанцируем один из классов менеджера шагомера, туда передаются SensorManager, и ссылка на себя как стандартный интерфейс SensorEventListener, зачем? далее будет понятно. Потом мы отображаем уведомление о работе шагомера, чтобы можно было нажать на него и открыть окно с количеством шагов.

После onCreate() вызывается onStartCommand() и он возвращает START_STICKY, что делает его «липким», то есть если его убивает система, он в теории должен перезапустится, но это только в теории.

Насчет хаков, так если посмотреть на метод onTaskRemoved, то можно увидеть один из возможных способов перезапуска сервиса «работающий» начиная с Kitkat 4.4 API 16:

Мы пытаемся создать задачу в AlarmManager на запуск самого себя через пол секунды, в надежде что сработает(Вообще гарантий нет, что сервис снова запустится).

А методе onSensorChanged находиться достаточно интересный код, который поможет ответить на вопрос — как сделать шагомер для Android устройств на акселерометре?:

Сюда приходят обработанные данные от одного из менеджеров, который либо обрабатываю сырые данные, либо работает как посредник с датчиком шагомера. Получая шаги проверяется входит ли шаг в период, который в данный момент 1 час, если период не начат, тогда он будет null и «неизвестные» шаги либо обнуляются, либо им устанавливается текущие пройденные шаги из предыдущих периодов, которые не должны попадать в этот период. Если период уже начался, тогда происходят уже другие расчеты. Далее период обновляется, если уже начался, иначе создается новый. Для обновления UI кидаем Intent.

Менеджеры для обработки шагов с сенсоров

Сначала представлю базовый класс для менеджеров:

Он нужен чтобы не дублировать код обоих менеджерах.

StepCounterManagerStepCounterSensor

Этот менеджер для простого проксирования шагов.

StepCounterManagerAccelerometer

Вот этот менеджер уже интереснее…
Мы регистрируемся на события акселерометра, читаем сырые данные с него, применяем LowPassFilter фильтр, вычисляем дельту для вещественной части комплексного числа.
Далее по кругу заполняем массив комплексных чисел, и когда он заполнится применяем FFT. У нас на выходе будет обработанные данные в виде массива комплексных чисел, мы берем первый элемент и проверяем входит ли вещественная часть числа в пороговые значения. Когда шаг идентифицирован на первом этапе, начинается проверка мнимой части на пороговые значения, и попадание в подобранное время задержки между шагами, если все попадает в диапазоны, происходит регистрация шага и отправкой суммы шагов в сервис.

Room и Репозиторий шагов

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

Создаем Dao для хранения шагов:

Первые два метода думаю объяснять не нужно, а вот с двумя другими могу возникнуть вопросы. В getCurrentPeriod() происходит попытка получить период для текущего часа, это все происходит в SQL запросе, спасибо SQLite. getTodayStepCount() суммирует шаги с начала суток до текущего времени.

Сами шаги хранятся в «Периодах», а он у нас час:

Класс базы данных:

Класс StepRepositoryImpl для доступа к данным:

В этом классе выполняется создание базы данных и делегирование ей операций над шагами.

Вот мы и сделали шагомер для Android устройств на акселерометре!
Использованный материал:

Весь код доступен на Github-е — Stepcounterwithwidget