Интеграция модуля Wear OS

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

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

Android Studio предоставляет удобный мастер для добавления модуля Wear OS в ваше приложение. В меню «Файл» > «Новый модуль» выберите Wear OS , как показано на следующем изображении:

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

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

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

Изображение с кнопкой запуска приложения Wear OS.
Рис. 2. Кнопка «Выполнить» для нового модуля Wear OS.

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

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

Добавьте медицинские услуги и Hilt

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

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

Создайте менеджера служб здравоохранения.

Чтобы сделать использование Health Services более удобным и предоставить более компактный и плавный 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
        }
    }

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

val heartRate by viewModel.hr

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

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

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

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