fix(wear): improve client initialization, consolidate health callbacks, and reset workout state
This commit is contained in:
+23
-9
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user