diff --git a/modules/wear-connectivity/android/src/main/java/nl/guidoit/swimbuddy/wear/WearConnectivityModule.kt b/modules/wear-connectivity/android/src/main/java/nl/guidoit/swimbuddy/wear/WearConnectivityModule.kt index a2f760f..512ff1a 100644 --- a/modules/wear-connectivity/android/src/main/java/nl/guidoit/swimbuddy/wear/WearConnectivityModule.kt +++ b/modules/wear-connectivity/android/src/main/java/nl/guidoit/swimbuddy/wear/WearConnectivityModule.kt @@ -21,6 +21,15 @@ class WearConnectivityModule : Module() { private set } + private fun ensureClients(): Pair? { + 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 { Name("WearConnectivity") @@ -43,7 +52,8 @@ class WearConnectivityModule : Module() { AsyncFunction("isPaired") { try { - val nodes = nodeClient?.connectedNodes?.await() ?: emptyList() + val (_, nc) = ensureClients() ?: return@AsyncFunction false + val nodes = nc.connectedNodes.await() nodes.isNotEmpty() } catch (e: Exception) { false @@ -52,7 +62,8 @@ class WearConnectivityModule : Module() { AsyncFunction("isReachable") { try { - val nodes = nodeClient?.connectedNodes?.await() ?: emptyList() + val (_, nc) = ensureClients() ?: return@AsyncFunction false + val nodes = nc.connectedNodes.await() nodes.any { it.isNearby } } catch (e: Exception) { false @@ -61,8 +72,9 @@ class WearConnectivityModule : Module() { AsyncFunction("sendWorkoutPlan") { plan: Map -> try { - val ctx = appContext.reactContext ?: return@AsyncFunction mapOf("error" to "No context") - 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 } ?: return@AsyncFunction mapOf("error" to "No nearby watch found") @@ -74,11 +86,11 @@ class WearConnectivityModule : Module() { } } - messageClient?.sendMessage( + mc.sendMessage( nearbyNode.id, MESSAGE_PATH, json.toString().toByteArray(Charsets.UTF_8) - )?.await() + ).await() mapOf("status" to "sent") } catch (e: Exception) { @@ -88,7 +100,9 @@ class WearConnectivityModule : Module() { AsyncFunction("sendWorkoutCompletion") { data: Map -> 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 } ?: return@AsyncFunction mapOf("error" to "No nearby watch found") @@ -100,11 +114,11 @@ class WearConnectivityModule : Module() { } } - messageClient?.sendMessage( + mc.sendMessage( nearbyNode.id, MESSAGE_PATH, json.toString().toByteArray(Charsets.UTF_8) - )?.await() + ).await() mapOf("status" to "sent") } catch (e: Exception) { diff --git a/targets/wearos/app/src/main/java/nl/guidoit/swimbuddy/HealthServicesManager.kt b/targets/wearos/app/src/main/java/nl/guidoit/swimbuddy/HealthServicesManager.kt index 2593d82..68b80b9 100644 --- a/targets/wearos/app/src/main/java/nl/guidoit/swimbuddy/HealthServicesManager.kt +++ b/targets/wearos/app/src/main/java/nl/guidoit/swimbuddy/HealthServicesManager.kt @@ -9,15 +9,28 @@ import androidx.health.services.client.data.DataType import androidx.health.services.client.data.ExerciseUpdate import androidx.health.services.client.data.ExerciseState 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.callbackFlow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.tasks.await class HealthServicesManager(context: Context) { private val healthClient = HealthServices.getClient(context) private val exerciseClient: ExerciseClient = healthClient.exerciseClient + private val hrChannel = Channel(Channel.CONFLATED) + private val calChannel = Channel(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 { return try { val capabilities = exerciseClient.getCapabilitiesAsync().await() @@ -50,38 +63,21 @@ class HealthServicesManager(context: Context) { exerciseClient.endExerciseAsync().await() } - fun heartRateFlow(): Flow = callbackFlow { - val callback = object : androidx.health.services.client.ExerciseUpdateCallback { - override fun onNewUpdate(update: ExerciseUpdate) { - 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 heartRateFlow(): Flow { + ensureCallbackRegistered() + return hrChannel.receiveAsFlow() } - fun caloriesFlow(): Flow = callbackFlow { - val callback = object : androidx.health.services.client.ExerciseUpdateCallback { - override fun onNewUpdate(update: ExerciseUpdate) { - update.getData(DataType.CALORIES)?.let { data -> - trySend(data.toDouble()) - } - } + fun caloriesFlow(): Flow { + ensureCallbackRegistered() + return calChannel.receiveAsFlow() + } - override fun onAvailabilityChanged(dataType: DataType, availability: DataPointAvailability) { - // Not used - } + private fun ensureCallbackRegistered() { + if (!callbackRegistered) { + exerciseClient.setUpdateCallback(combinedCallback) + callbackRegistered = true } - - exerciseClient.setUpdateCallback(callback) - awaitClose { exerciseClient.clearUpdateCallbackAsync() } } fun isActive(): Boolean { diff --git a/targets/wearos/app/src/main/java/nl/guidoit/swimbuddy/WorkoutViewModel.kt b/targets/wearos/app/src/main/java/nl/guidoit/swimbuddy/WorkoutViewModel.kt index 35616e4..51f58da 100644 --- a/targets/wearos/app/src/main/java/nl/guidoit/swimbuddy/WorkoutViewModel.kt +++ b/targets/wearos/app/src/main/java/nl/guidoit/swimbuddy/WorkoutViewModel.kt @@ -187,6 +187,7 @@ class WorkoutViewModel(application: Application) : AndroidViewModel(application) } fun completeLap() { + if (!_isWorkoutActive.value || _isResting.value) return val workout = _selectedWorkout.value ?: return val currentSet = workout.sets.getOrNull(_currentSetIndex.value) ?: return @@ -245,6 +246,9 @@ class WorkoutViewModel(application: Application) : AndroidViewModel(application) viewModelScope.launch { healthManager.endWorkout() _isWorkoutActive.value = false + _currentSetIndex.value = 0 + _currentRep.value = 0 + _elapsedSeconds.value = 0f HapticFeedback.play(appContext, HapticFeedback.Type.SUCCESS)