Как интегрировать модуль Wear OS

Сделайте свое приложение для здоровья и фитнеса ещё удобнее, добавив поддержку носимых устройств на базе Wear OS.

Добавьте модуль Wear OS

В Android Studio есть удобный мастер, который позволяет добавить модуль Wear OS в приложение. В меню File (Файл) > New Module (Новый модуль) выберите Wear OS, как показано на изображении ниже.

Мастер модулей Wear OS в Android Studio
Рисунок 1. Создание модуля Wear OS

Обратите внимание, что для использования последней версии сервисов наблюдения за здоровьем минимальная версия SDK должна быть API 30 или выше. Сервисы наблюдения за здоровьем автоматически настраивают датчики, чтобы вам было проще отслеживать показатели и записывать данные.

После завершения работы мастера синхронизируйте проект. Появится следующая конфигурация Run (Запуск):

Изображение, на котором показана кнопка запуска приложения для Wear OS
Рисунок 2. Кнопка запуска нового модуля Wear OS

Это позволит вам запустить модуль Wear OS на носимом устройстве. Доступны два варианта:

При запуске конфигурации приложение развертывается в эмуляторе или на устройстве Wear OS и показывает приветствие. Это базовая настройка интерфейса с использованием Compose для Wear OS, которая поможет вам начать работу с приложением.

Добавьте сервисы наблюдения за здоровьем и Hilt

Интегрируйте в модуль Wear OS следующие библиотеки:

  • Сервисы наблюдения за здоровьем упрощают доступ к датчикам и данным на часах и снижают энергопотребление.
  • Hilt – библиотека для эффективного внедрения зависимостей и управления ими.

Создайте менеджер сервисов наблюдения за здоровьем

Чтобы упростить работу с сервисами наблюдения за здоровьем и сделать API более компактным и удобным, можно создать оболочку, как показано ниже.

private const val TAG = "WATCHMAIN"

class HealthServicesManager(context: Context) {
    private val measureClient = HealthServices.getClient(context).measureClient

    suspend fun hasHeartRateCapability() = runCatching {
        val capabilities = measureClient.getCapabilities()
        (DataType.HEART_RATE_BPM in capabilities.supportedDataTypesMeasure)
    }.getOrDefault(false)

    /**
     * Returns a cold flow. When activated, the flow will register a callback for heart rate data
     * and start to emit messages. When the consuming coroutine is canceled, the measure callback
     * is unregistered.
     *
     * [callbackFlow] creates a  bridge between a callback-based API and Kotlin flows.
     */
    @ExperimentalCoroutinesApi
    fun heartRateMeasureFlow(): Flow<MeasureMessage> = callbackFlow {
        val callback = object : MeasureCallback {
            override fun onAvailabilityChanged(dataType: DeltaDataType<*, *>, availability: Availability) {
                // Only send back DataTypeAvailability (not LocationAvailability)
                if (availability is DataTypeAvailability) {
                    trySendBlocking(MeasureMessage.MeasureAvailability(availability))
                }
            }

            override fun onDataReceived(data: DataPointContainer) {
                val heartRateBpm = data.getData(DataType.HEART_RATE_BPM)
                Log.d(TAG, "💓 Received heart rate: ${heartRateBpm.first().value}")
                trySendBlocking(MeasureMessage.MeasureData(heartRateBpm))
            }
        }

        Log.d(TAG, "⌛ Registering for data...")
        measureClient.registerMeasureCallback(DataType.HEART_RATE_BPM, callback)

        awaitClose {
            Log.d(TAG, "👋 Unregistering for data")
            runBlocking {
                measureClient.unregisterMeasureCallback(DataType.HEART_RATE_BPM, callback)
            }
        }
    }
}

sealed class MeasureMessage {
    class MeasureAvailability(val availability: DataTypeAvailability) : MeasureMessage()
    class MeasureData(val data: List<SampleDataPoint<Double>>) : MeasureMessage()
}

После создания модуля Hilt для управления им используйте следующий фрагмент кода:

@Module
@InstallIn(SingletonComponent::class)
internal object DataModule {
    @Provides
    @Singleton
    fun provideHealthServices(@ApplicationContext context: Context): HealthServicesManager = HealthServicesManager(context)
}

Вы можете внедрить HealthServicesManager как любую другую зависимость Hilt.

Новый класс HealthServicesManager предоставляет метод heartRateMeasureFlow(), который регистрирует прослушиватель для пульсометра и передает полученные данные.

Включить обновление данных на носимых устройствах

Для обновления данных о фитнесе требуется разрешение BODY_SENSORS. Если вы ещё этого не сделали, объявите разрешение BODY_SENSORS в файле манифеста приложения. Затем запросите разрешение, как показано в этом фрагменте:

val permissionState = rememberPermissionState(
    permission = Manifest.permission.BODY_SENSORS,
    onPermissionResult = { granted -> /* do something */ }
)

[...]

if (permissionState.status.isGranted) {
    // do something
} else {
    permissionState.launchPermissionRequest()
}

Если вы тестируете приложение на физическом устройстве, данные должны начать обновляться.

Начиная с Wear OS 4, эмуляторы также автоматически показывают тестовые данные. В предыдущих версиях можно симулировать поток данных от датчика. В окне терминала выполните следующую команду ADB:

adb shell am broadcast \
-a "whs.USE_SYNTHETIC_PROVIDERS" \
com.google.android.wearable.healthservices

Чтобы увидеть разные значения пульса, попробуйте симулировать разные упражнения. Эта команда имитирует ходьбу:

adb shell am broadcast \
-a "whs.synthetic.user.START_WALKING" \
com.google.android.wearable.healthservices

Эта команда имитирует запуск:

adb shell am broadcast \
-a "whs.synthetic.user.START_RUNNING" \
com.google.android.wearable.healthservices

Чтобы остановить симуляцию, выполните следующую команду:

adb shell am broadcast -a \
"whs.USE_SENSOR_PROVIDERS" \
com.google.android.wearable.healthservices

Чтение данных о пульсе

Если разрешение BODY_SENSORS предоставлено, вы можете считывать данные о пульсе пользователя (heartRateMeasureFlow()) в HealthServicesManager. В интерфейсе приложения для Wear OS появится текущее значение пульса, измеренное датчиком на носимом устройстве.

В файле ViewModel начните сбор данных с помощью объекта потока пульса, как показано в следующем фрагменте кода:

val hr: MutableState<Double> = mutableStateOf(0.0)

[...]

healthServicesManager
    .heartRateMeasureFlow()
    .takeWhile { enabled.value }
    .collect { measureMessage ->
        when (measureMessage) {
            is MeasureData -> {
                val latestHeartRateValue = measureMessage.data.last().value
                hr.value = latestHeartRateValue
            }

            is MeasureAvailability -> availability.value =
                    measureMessage.availability
        }
    }

Чтобы отобразить данные в реальном времени в интерфейсе приложения, используйте composable-функцию, похожую на следующий объект:

val heartRate by viewModel.hr

Text(
  text = "Heart Rate: $heartRate",
  style = MaterialTheme.typography.display1
)

Отправка данных на мобильное устройство

Чтобы отправить данные о здоровье и физической активности на мобильное устройство, используйте класс DataClient в сервисах наблюдения за здоровьем. В следующем фрагменте кода показано, как отправить данные о пульсе, которые ваше приложение собрало ранее:

class HealthServicesManager(context: Context) {
    private val dataClient by lazy { Wearable.getDataClient(context) }

[...]

    suspend fun sendToHandheldDevice(heartRate: Int) {
        try {
            val result = dataClient
                .putDataItem(PutDataMapRequest
                    .create("/heartrate")
                    .apply { dataMap.putInt("heartrate", heartRate) }
                    .asPutDataRequest()
                    .setUrgent())
                .await()

            Log.d(TAG, "DataItem saved: $result")
        } catch (cancellationException: CancellationException) {
            throw cancellationException
        } catch (exception: Exception) {
            Log.d(TAG, "Saving DataItem failed: $exception")
        }
    }
}

Получение данных на телефоне

Чтобы получать данные на телефоне, создайте WearableListenerService.

@AndroidEntryPoint
class DataLayerListenerService : WearableListenerService() {

    @Inject
    lateinit var heartRateMonitor: HeartRateMonitor

    override fun onDataChanged(dataEvents: DataEventBuffer) {

        dataEvents.forEach { event ->
            when (event.type) {
                DataEvent.TYPE_CHANGED -> {
                    event.dataItem.run {
                        if (uri.path?.compareTo("/heartrate") == 0) {
                            val heartRate = DataMapItem.fromDataItem(this)
                                    .dataMap.getInt(HR_KEY)
                            Log.d("DataLayerListenerService",
                                    "New heart rate value received: $heartRate")
                            heartRateMonitor.send(heartRate)
                        }
                    }
                }

                DataEvent.TYPE_DELETED -> {
                    // DataItem deleted
                }
            }
        }
    }
}

После выполнения этого шага обратите внимание на несколько интересных деталей:

  • Аннотация @AndroidEntryPoint позволяет использовать Hilt в этом классе.
  • @Inject lateinit var heartRateMonitor: HeartRateMonitor действительно внедрит зависимость в этот класс
  • Класс реализует onDataChanged() и получает набор событий, которые можно проанализировать и использовать.

Следующая логика HeartRateMonitor позволяет отправлять полученные значения пульса в другую часть базы кода приложения:

class HeartRateMonitor {
    private val datapoints = MutableSharedFlow<Int>(extraBufferCapacity = 10)

    fun receive(): SharedFlow<Int> = datapoints.asSharedFlow()

    fun send(hr: Int) {
        datapoints.tryEmit(hr)
    }
}

Шина данных получает события от метода onDataChanged() и делает их доступными для наблюдателей данных с помощью SharedFlow.

Последний шаг – объявление Service в приложении для телефона AndroidManifest.xml:

<service
    android:name=".DataLayerListenerService"
    android:exported="true">
    <intent-filter>
        <!-- listeners receive events that match the action and data filters -->
        <action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
        <data
            android:host="*"
            android:pathPrefix="/heartrate"
            android:scheme="wear" />
    </intent-filter>
</service>

Как показывать данные в реальном времени на мобильном устройстве

В части приложения, которая работает на мобильном устройстве, вставьте HeartRateMonitor в конструктор модели представления. Этот объект HeartRateMonitor отслеживает данные о пульсе и при необходимости обновляет интерфейс.