Measure and Transfer Heart Rate Data from Galaxy Watch to a Paired Android Phone

Samsung Developers

The Samsung Privileged Health SDK enables your application to collect vital signs and other health parameters tracked on Galaxy Watch running Wear OS powered by Samsung. The tracked data can be displayed immediately or retained for later analysis. Some kinds of tracked data, such as batching data, are impractical to display on a watch screen in real-time, so it is common to store the data in a database or server solution or show them on the larger screen of a mobile device.

This blog demonstrates how to develop 2 connected sample applications. A watch application uses the Samsung Privileged Health SDK to collect heart rate tracker data, then uses the Wearable Data Layer API to transmit it to a companion application on the user’s Android mobile device, which displays the data as a simple list on its screen.

You can follow along with the demonstration by downloading the sample application project. To test the applications, you need a Galaxy Watch4 (or higher model) and a connected Android mobile device.

Creating the application project

The application project consists of a wearable module for the watch, and a mobile module for Android mobile devices:

  1. In Android Studio, select Open File > New > New Project.

  2. Select Wear OS > Empty Wear App and click Next.

    New Wear app


  3. Define the project details.

    Project details


  4. To create a companion mobile application for the watch application, check the Pair with Empty Phone app box.

For more information about creating multi-module projects, see From Wrist to Hand: Develop a Companion App for Your Wearable Application.

Implementing the watch application

The watch application UI has 2 buttons. The Start/Stop button controls heart data tracking, and the Send button transfers the collected data to the connected mobile device. The screen consists of a heart rate field and 4 IBI value fields, since there can be up to 4 IBI values in a single tracking result.

Watch application UI

Track and extract heart rate data

When the user taps the Start button on the wearable application UI, the startTracking() function from the MainViewModel class is invoked.

The application must check that the Galaxy Watch supports the heart rate tracking capability that we want to implement, as the supported capabilities depend on the device model and software version.

Retrieve the list of supported health trackers with the trackingCapability.supportHealthTrackerTypes of the HealthTrackingService class:

override fun hasCapabilities(): Boolean {
    Log.i(TAG, "hasCapabilities()")
    healthTrackingService = healthTrackingServiceConnection.getHealthTrackingService()
    val trackers: List<HealthTrackerType> =
        healthTrackingService!!.trackingCapability.supportHealthTrackerTypes
    return trackers.contains(trackingType)
}

To track the heart rate values on the watch, read the flow of values received in the onDataReceived() listener:

@ExperimentalCoroutinesApi
override suspend fun track(): Flow<TrackerMessage> = callbackFlow {
    val updateListener = object : HealthTracker.TrackerEventListener {
        override fun onDataReceived(dataPoints: MutableList<DataPoint>) {

            for (dataPoint in dataPoints) {

                var trackedData: TrackedData? = null
                val hrValue = dataPoint.getValue(ValueKey.HeartRateSet.HEART_RATE)
                val hrStatus = dataPoint.getValue(ValueKey.HeartRateSet.HEART_RATE_STATUS)

                if (isHRValid(hrStatus)) {
                    trackedData = TrackedData()
                    trackedData.hr = hrValue
                    Log.i(TAG, "valid HR: $hrValue")
                } else {
                    coroutineScope.runCatching {
                        trySendBlocking(TrackerMessage.TrackerWarningMessage(getError(hrStatus.toString())))
                    }
                }

                val validIbiList = getValidIbiList(dataPoint)
                if (validIbiList.size > 0) {
                    if (trackedData == null) trackedData = TrackedData()
                    trackedData.ibi.addAll(validIbiList)
                }

                if ((isHRValid(hrStatus) || validIbiList.size > 0) && trackedData != null) {
                    coroutineScope.runCatching {
                        trySendBlocking(TrackerMessage.DataMessage(trackedData))
                    }
                }
                if (trackedData != null) {
                    validHrData.add(trackedData)
                }
            }
            trimDataList()
        }

        fun getError(errorKeyFromTracker: String): String {
            val str = errors.getValue(errorKeyFromTracker)
            return context.resources.getString(str)
        }

        override fun onFlushCompleted() {

            Log.i(TAG, "onFlushCompleted()")
            coroutineScope.runCatching {
                trySendBlocking(TrackerMessage.FlushCompletedMessage)
            }
        }

        override fun onError(trackerError: HealthTracker.TrackerError?) {

            Log.i(TAG, "onError()")
            coroutineScope.runCatching {
                trySendBlocking(TrackerMessage.TrackerErrorMessage(getError(trackerError.toString())))
            }
        }
    }

    heartRateTracker =
        healthTrackingService!!.getHealthTracker(trackingType)

    setListener(updateListener)

    awaitClose {
        Log.i(TAG, "Tracking flow awaitClose()")
        stopTracking()
    }
}

Each tracking result is within a list in the DataPoints argument of the onDataReceived() update listener. The sample application implements on-demand heart rate tracking, the update listener is invoked every second and each data point list contains 1 element.

To extract a heart rate from data point:

val hrValue = dataPoint.getValue(ValueKey.HeartRateSet.HEART_RATE)
val hrStatus = dataPoint.getValue(ValueKey.HeartRateSet.HEART_RATE_STATUS)

A status parameter is returned in addition to the heart rate data. If the heart rate reading was successful, its value is 1.

Each inter-beat interval data point consists of a list of values and the corresponding status for each value. Since Samsung Privileged Health SDK version 1.2.0, there can be up to 4 IBI values in a single data point, depending on the heart rate. If the IBI reading is valid, the value of the status parameter is 0.

To extract only IBI data that is valid and whose value is not 0:

private fun isIBIValid(ibiStatus: Int, ibiValue: Int): Boolean {
    return ibiStatus == 0 && ibiValue != 0
}

fun getValidIbiList(dataPoint: DataPoint): ArrayList<Int> {

    val ibiValues = dataPoint.getValue(ValueKey.HeartRateSet.IBI_LIST)
    val ibiStatuses = dataPoint.getValue(ValueKey.HeartRateSet.IBI_STATUS_LIST)

    val validIbiList = ArrayList<Int>()
    for ((i, ibiStatus) in ibiStatuses.withIndex()) {
        if (isIBIValid(ibiStatus, ibiValues[i])) {
            validIbiList.add(ibiValues[i])
        }
    }

Send data to the mobile application

The application uses the MessageClient class of the Wearable Data Layer API to send messages to the connected mobile device. Messages are useful for remote procedure calls (RPC), one-way requests, or in request-or-response communication models. When a message is sent, if the sending and receiving devices are connected, the system queues the message for delivery and returns a successful result code. The successful result code does not necessarily mean that the message was delivered successfully, as the devices can be disconnected before the message is received.

To advertise and discover devices on the same network with features that the watch can interact with, use the CapabilityClient class of the Wearable Data Layer API. Each device on the network is represented as a node that supports various capabilities (features) that an application defines at build time or configures dynamically at runtime. Your watch application can search for nodes with a specific capability and interact with it, such as sending messages. This can also work in the opposite direction, with the wearable application advertising the capabilities it supports.

When the user taps the Send button on the wearable application UI, the sendMessage() function from the MainViewModel class is invoked, which triggers code in the SendMessageUseCase class:

override suspend fun sendMessage(message: String, node: Node, messagePath: String): Boolean {
    val nodeId = node.id
    var result = false
    nodeId.also { id ->
        messageClient
            .sendMessage(
                id,
                messagePath,
                message.toByteArray(charset = Charset.defaultCharset())
            ).apply {
                addOnSuccessListener {
                    Log.i(TAG, "sendMessage OnSuccessListener")
                    result = true
                }
                addOnFailureListener {
                    Log.i(TAG, "sendMessage OnFailureListener")
                    result = false
                }
            }.await()
        Log.i(TAG, "result: $result")
        return result
    }
}

To find a destination node for the message, retrieve all the available capabilities on the network:

override suspend fun getCapabilitiesForReachableNodes(): Map<Node, Set<String>> {
    Log.i(TAG, "getCapabilities()")

    val allCapabilities =
        capabilityClient.getAllCapabilities(CapabilityClient.FILTER_REACHABLE).await()

    return allCapabilities.flatMap { (capability, capabilityInfo) ->
        capabilityInfo.nodes.map {
            it to capability
        }
    }
        .groupBy(
            keySelector = { it.first },
            valueTransform = { it.second }
        )
        .mapValues { it.value.toSet() }
}

Since the mobile module of the sample application advertises having the “wear” capability, to find an appropriate destination node, retrieve the list of connected nodes that support it:

override suspend fun getNodesForCapability(
    capability: String,
    allCapabilities: Map<Node, Set<String>>
): Set<Node> {
    return allCapabilities.filterValues { capability in it }.keys
}

Select the first node from the list, encode the message as a JSON string, and send the message to the node:

suspend operator fun invoke(): Boolean {

    val nodes = getCapableNodes()

    return if (nodes.isNotEmpty()) {

        val node = nodes.first()
        val message =
            encodeMessage(trackingRepository.getValidHrData())
        messageRepository.sendMessage(message, node, MESSAGE_PATH)

        true

    } else {
        Log.i(TAG, "No compatible nodes found")
        false
    }
}

Implementing the mobile application

The mobile application UI consists of a list of the heart rate and inter-beat interval values received from the watch. The list is scrollable.

Mobile application UI

Receive and display data from the watch application

To enable the mobile application to listen for data from the watch and launch when it receives data, define the DataListenerService service in the mobile application’s AndroidManifest.xml file, within the <application> element:

 <service
   android:name="com.samsung.health.mobile.data.DataListenerService"
   android:exported="true">

   <intent-filter>
       <action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
       <action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
       <action android:name="com.google.android.gms.wearable.REQUEST_RECEIVED" />
       <action android:name="com.google.android.gms.wearable.CAPABILITY_CHANGED" />
       <action android:name="com.google.android.gms.wearable.CHANNEL_EVENT" />

       <data
           android:host="*"
           android:pathPrefix="/msg"
           android:scheme="wear" />
   </intent-filter>
</service>

Implement the DataListenerService class in the application code to listen for and receive message data. The received JSON string data is passed as a parameter:

private const val TAG = "DataListenerService"
private const val MESSAGE_PATH = "/msg"

class DataListenerService : WearableListenerService() {

   override fun onMessageReceived(messageEvent: MessageEvent) {
       super.onMessageReceived(messageEvent)

       val value = messageEvent.data.decodeToString()
       Log.i(TAG, "onMessageReceived(): $value")
       when (messageEvent.path) {
           MESSAGE_PATH -> {
               Log.i(TAG, "Service: message (/msg) received: $value")

               if (value != "") {
                   startActivity(
                       Intent(this, MainActivity::class.java)
                           .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).putExtra("message", value)
                   )
               } else {
                   Log.i(TAG, "value is an empty string")
               }
           }
       }

To decode the message data:

fun decodeMessage(message: String): List<TrackedData> {

    return Json.decodeFromString(message)
}

To display the received data on the application screen:

@Composable
fun MainScreen(
    results: List<TrackedData>
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black),
        verticalArrangement = Arrangement.Top,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Spacer(
            Modifier
                .height(70.dp)
                .fillMaxWidth()
                .background(Color.Black)
        )
        ListView(results)
    }
}

Running the applications

To run the wearable and mobile applications:

  1. Connect your Galaxy Watch and Android mobile device (both devices must be paired with each other) to Android Studio on your computer.
  2. Select wear from the modules list and the Galaxy Watch device from the devices list, then click Run. The wearable application launches on the watch.

    Connected devices


  3. Select mobile from the modules list and the Android mobile device from the devices list, then click Run. The mobile application launches on the mobile device.
  4. Wear the watch on your wrist and tap Start. The watch begins tracking your heart rate. After some tracked values appear on the watch screen, to send the values to the mobile application, tap Send. If the mobile application is not running, it is launched. The tracked heart data appears on the mobile application screen.
  5. To stop tracking, tap Stop on the watch.

Conclusions

The Samsung Privileged Health SDK enables you to track health data, such as heart rate, from a user’s Galaxy Watch4 or higher smartwatch model. To display the tracked data on a larger screen, you can use the MessageClient of the Wearable Data Layer API to send the data to a companion application on the connected mobile device.

To develop more advanced application features, you can also use the DataClient class to send data to devices not currently in range of the watch, delivering it only when the device is connected.

Resources

Heart Rate Data Transfer Code Lab