fix(wear): improve client initialization, consolidate health callbacks, and reset workout state

This commit is contained in:
2026-08-08 12:42:06 +02:00
parent 262d7d7fe4
commit 065a564e1c
3 changed files with 53 additions and 39 deletions
@@ -21,6 +21,15 @@ class WearConnectivityModule : Module() {
private set private set
} }
private fun ensureClients(): Pair<MessageClient, NodeClient>? {
if (messageClient == null || nodeClient == null) {
val ctx = appContext.reactContext ?: return null
messageClient = messageClient ?: Wearable.getMessageClient(ctx)
nodeClient = nodeClient ?: Wearable.getNodeClient(ctx)
}
return messageClient!! to nodeClient!!
}
override fun definition() = ModuleDefinition { override fun definition() = ModuleDefinition {
Name("WearConnectivity") Name("WearConnectivity")
@@ -43,7 +52,8 @@ class WearConnectivityModule : Module() {
AsyncFunction("isPaired") { AsyncFunction("isPaired") {
try { try {
val nodes = nodeClient?.connectedNodes?.await() ?: emptyList() val (_, nc) = ensureClients() ?: return@AsyncFunction false
val nodes = nc.connectedNodes.await()
nodes.isNotEmpty() nodes.isNotEmpty()
} catch (e: Exception) { } catch (e: Exception) {
false false
@@ -52,7 +62,8 @@ class WearConnectivityModule : Module() {
AsyncFunction("isReachable") { AsyncFunction("isReachable") {
try { try {
val nodes = nodeClient?.connectedNodes?.await() ?: emptyList() val (_, nc) = ensureClients() ?: return@AsyncFunction false
val nodes = nc.connectedNodes.await()
nodes.any { it.isNearby } nodes.any { it.isNearby }
} catch (e: Exception) { } catch (e: Exception) {
false false
@@ -61,8 +72,9 @@ class WearConnectivityModule : Module() {
AsyncFunction("sendWorkoutPlan") { plan: Map<String, Any?> -> AsyncFunction("sendWorkoutPlan") { plan: Map<String, Any?> ->
try { try {
val ctx = appContext.reactContext ?: return@AsyncFunction mapOf("error" to "No context") val (mc, nc) = ensureClients()
val nodes = nodeClient?.connectedNodes?.await() ?: emptyList() ?: return@AsyncFunction mapOf("error" to "No context")
val nodes = nc.connectedNodes.await()
val nearbyNode = nodes.firstOrNull { it.isNearby } val nearbyNode = nodes.firstOrNull { it.isNearby }
?: return@AsyncFunction mapOf("error" to "No nearby watch found") ?: return@AsyncFunction mapOf("error" to "No nearby watch found")
@@ -74,11 +86,11 @@ class WearConnectivityModule : Module() {
} }
} }
messageClient?.sendMessage( mc.sendMessage(
nearbyNode.id, nearbyNode.id,
MESSAGE_PATH, MESSAGE_PATH,
json.toString().toByteArray(Charsets.UTF_8) json.toString().toByteArray(Charsets.UTF_8)
)?.await() ).await()
mapOf("status" to "sent") mapOf("status" to "sent")
} catch (e: Exception) { } catch (e: Exception) {
@@ -88,7 +100,9 @@ class WearConnectivityModule : Module() {
AsyncFunction("sendWorkoutCompletion") { data: Map<String, Any?> -> AsyncFunction("sendWorkoutCompletion") { data: Map<String, Any?> ->
try { try {
val nodes = nodeClient?.connectedNodes?.await() ?: emptyList() val (mc, nc) = ensureClients()
?: return@AsyncFunction mapOf("error" to "No context")
val nodes = nc.connectedNodes.await()
val nearbyNode = nodes.firstOrNull { it.isNearby } val nearbyNode = nodes.firstOrNull { it.isNearby }
?: return@AsyncFunction mapOf("error" to "No nearby watch found") ?: return@AsyncFunction mapOf("error" to "No nearby watch found")
@@ -100,11 +114,11 @@ class WearConnectivityModule : Module() {
} }
} }
messageClient?.sendMessage( mc.sendMessage(
nearbyNode.id, nearbyNode.id,
MESSAGE_PATH, MESSAGE_PATH,
json.toString().toByteArray(Charsets.UTF_8) json.toString().toByteArray(Charsets.UTF_8)
)?.await() ).await()
mapOf("status" to "sent") mapOf("status" to "sent")
} catch (e: Exception) { } catch (e: Exception) {
@@ -9,15 +9,28 @@ import androidx.health.services.client.data.DataType
import androidx.health.services.client.data.ExerciseUpdate import androidx.health.services.client.data.ExerciseUpdate
import androidx.health.services.client.data.ExerciseState import androidx.health.services.client.data.ExerciseState
import androidx.health.services.client.data.DataPointAvailability import androidx.health.services.client.data.DataPointAvailability
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.tasks.await import kotlinx.coroutines.tasks.await
class HealthServicesManager(context: Context) { class HealthServicesManager(context: Context) {
private val healthClient = HealthServices.getClient(context) private val healthClient = HealthServices.getClient(context)
private val exerciseClient: ExerciseClient = healthClient.exerciseClient private val exerciseClient: ExerciseClient = healthClient.exerciseClient
private val hrChannel = Channel<Double>(Channel.CONFLATED)
private val calChannel = Channel<Double>(Channel.CONFLATED)
private var callbackRegistered = false
private val combinedCallback = object : androidx.health.services.client.ExerciseUpdateCallback {
override fun onNewUpdate(update: ExerciseUpdate) {
update.getData(DataType.HEART_RATE_BPM)?.toDouble()?.let { hrChannel.trySend(it) }
update.getData(DataType.CALORIES)?.toDouble()?.let { calChannel.trySend(it) }
}
override fun onAvailabilityChanged(dataType: DataType, availability: DataPointAvailability) {}
}
suspend fun hasSwimmingCapability(): Boolean { suspend fun hasSwimmingCapability(): Boolean {
return try { return try {
val capabilities = exerciseClient.getCapabilitiesAsync().await() val capabilities = exerciseClient.getCapabilitiesAsync().await()
@@ -50,38 +63,21 @@ class HealthServicesManager(context: Context) {
exerciseClient.endExerciseAsync().await() exerciseClient.endExerciseAsync().await()
} }
fun heartRateFlow(): Flow<Double> = callbackFlow { fun heartRateFlow(): Flow<Double> {
val callback = object : androidx.health.services.client.ExerciseUpdateCallback { ensureCallbackRegistered()
override fun onNewUpdate(update: ExerciseUpdate) { return hrChannel.receiveAsFlow()
update.getData(DataType.HEART_RATE_BPM)?.let { data ->
trySend(data.toDouble())
}
}
override fun onAvailabilityChanged(dataType: DataType, availability: DataPointAvailability) {
// Not used
}
}
exerciseClient.setUpdateCallback(callback)
awaitClose { exerciseClient.clearUpdateCallbackAsync() }
} }
fun caloriesFlow(): Flow<Double> = callbackFlow { fun caloriesFlow(): Flow<Double> {
val callback = object : androidx.health.services.client.ExerciseUpdateCallback { ensureCallbackRegistered()
override fun onNewUpdate(update: ExerciseUpdate) { return calChannel.receiveAsFlow()
update.getData(DataType.CALORIES)?.let { data -> }
trySend(data.toDouble())
}
}
override fun onAvailabilityChanged(dataType: DataType, availability: DataPointAvailability) { private fun ensureCallbackRegistered() {
// Not used if (!callbackRegistered) {
} exerciseClient.setUpdateCallback(combinedCallback)
callbackRegistered = true
} }
exerciseClient.setUpdateCallback(callback)
awaitClose { exerciseClient.clearUpdateCallbackAsync() }
} }
fun isActive(): Boolean { fun isActive(): Boolean {
@@ -187,6 +187,7 @@ class WorkoutViewModel(application: Application) : AndroidViewModel(application)
} }
fun completeLap() { fun completeLap() {
if (!_isWorkoutActive.value || _isResting.value) return
val workout = _selectedWorkout.value ?: return val workout = _selectedWorkout.value ?: return
val currentSet = workout.sets.getOrNull(_currentSetIndex.value) ?: return val currentSet = workout.sets.getOrNull(_currentSetIndex.value) ?: return
@@ -245,6 +246,9 @@ class WorkoutViewModel(application: Application) : AndroidViewModel(application)
viewModelScope.launch { viewModelScope.launch {
healthManager.endWorkout() healthManager.endWorkout()
_isWorkoutActive.value = false _isWorkoutActive.value = false
_currentSetIndex.value = 0
_currentRep.value = 0
_elapsedSeconds.value = 0f
HapticFeedback.play(appContext, HapticFeedback.Type.SUCCESS) HapticFeedback.play(appContext, HapticFeedback.Type.SUCCESS)