feat(watch-connectivity): add WearOS support alongside Apple Watch
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="nl.guidoit.swimbuddy.wear">
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name=".WearMessageListenerService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
|
||||
<data
|
||||
android:scheme="wear"
|
||||
android:host="*"
|
||||
android:pathPrefix="/swimbuddy" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package nl.guidoit.swimbuddy.wear
|
||||
|
||||
import com.google.android.gms.wearable.MessageClient
|
||||
import com.google.android.gms.wearable.MessageEvent
|
||||
import com.google.android.gms.wearable.NodeClient
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import org.json.JSONObject
|
||||
|
||||
class WearConnectivityModule : Module() {
|
||||
private var messageClient: MessageClient? = null
|
||||
private var nodeClient: NodeClient? = null
|
||||
private val pendingEvents = mutableListOf<Map<String, Any?>>()
|
||||
|
||||
private val MESSAGE_PATH = "/swimbuddy"
|
||||
|
||||
companion object {
|
||||
var instance: WearConnectivityModule? = null
|
||||
private set
|
||||
}
|
||||
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("WearConnectivity")
|
||||
|
||||
OnCreate {
|
||||
instance = this@WearConnectivityModule
|
||||
val ctx = appContext.reactContext ?: return@OnCreate
|
||||
messageClient = Wearable.getMessageClient(ctx)
|
||||
nodeClient = Wearable.getNodeClient(ctx)
|
||||
}
|
||||
|
||||
AsyncFunction("isSupported") {
|
||||
try {
|
||||
val ctx = appContext.reactContext ?: return@AsyncFunction false
|
||||
com.google.android.gms.common.GoogleApiAvailability.getInstance()
|
||||
.isGooglePlayServicesAvailable(ctx) == com.google.android.gms.common.ConnectionResult.SUCCESS
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("isPaired") {
|
||||
try {
|
||||
val nodes = nodeClient?.connectedNodes?.await() ?: emptyList()
|
||||
nodes.isNotEmpty()
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("isReachable") {
|
||||
try {
|
||||
val nodes = nodeClient?.connectedNodes?.await() ?: emptyList()
|
||||
nodes.any { it.isNearby }
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("sendWorkoutPlan") { plan: Map<String, Any?> ->
|
||||
try {
|
||||
val ctx = appContext.reactContext ?: return@AsyncFunction mapOf("error" to "No context")
|
||||
val nodes = nodeClient?.connectedNodes?.await() ?: emptyList()
|
||||
val nearbyNode = nodes.firstOrNull { it.isNearby }
|
||||
?: return@AsyncFunction mapOf("error" to "No nearby watch found")
|
||||
|
||||
val json = JSONObject()
|
||||
json.put("type", "workoutPlan")
|
||||
for ((key, value) in plan) {
|
||||
if (value != null) {
|
||||
json.put(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
messageClient?.sendMessage(
|
||||
nearbyNode.id,
|
||||
MESSAGE_PATH,
|
||||
json.toString().toByteArray(Charsets.UTF_8)
|
||||
)?.await()
|
||||
|
||||
mapOf("status" to "sent")
|
||||
} catch (e: Exception) {
|
||||
mapOf("error" to (e.message ?: "Failed to send"))
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("sendWorkoutCompletion") { data: Map<String, Any?> ->
|
||||
try {
|
||||
val nodes = nodeClient?.connectedNodes?.await() ?: emptyList()
|
||||
val nearbyNode = nodes.firstOrNull { it.isNearby }
|
||||
?: return@AsyncFunction mapOf("error" to "No nearby watch found")
|
||||
|
||||
val json = JSONObject()
|
||||
json.put("type", "workoutCompleted")
|
||||
for ((key, value) in data) {
|
||||
if (value != null) {
|
||||
json.put(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
messageClient?.sendMessage(
|
||||
nearbyNode.id,
|
||||
MESSAGE_PATH,
|
||||
json.toString().toByteArray(Charsets.UTF_8)
|
||||
)?.await()
|
||||
|
||||
mapOf("status" to "sent")
|
||||
} catch (e: Exception) {
|
||||
mapOf("error" to (e.message ?: "Failed to send"))
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("pollEvents") {
|
||||
synchronized(pendingEvents) {
|
||||
val copy = ArrayList(pendingEvents)
|
||||
pendingEvents.clear()
|
||||
copy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleIncomingMessage(messageEvent: MessageEvent) {
|
||||
if (messageEvent.path != MESSAGE_PATH) return
|
||||
|
||||
try {
|
||||
val json = String(messageEvent.data, Charsets.UTF_8)
|
||||
val obj = JSONObject(json)
|
||||
val map = mutableMapOf<String, Any?>()
|
||||
for (key in obj.keys()) {
|
||||
map[key] = if (obj.isNull(key)) null else obj.get(key)
|
||||
}
|
||||
synchronized(pendingEvents) {
|
||||
pendingEvents.add(map)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ignore malformed messages
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
instance = null
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package nl.guidoit.swimbuddy.wear
|
||||
|
||||
import com.google.android.gms.wearable.MessageEvent
|
||||
import com.google.android.gms.wearable.WearableListenerService
|
||||
|
||||
class WearMessageListenerService : WearableListenerService() {
|
||||
|
||||
override fun onMessageReceived(messageEvent: MessageEvent) {
|
||||
WearConnectivityModule.instance?.handleIncomingMessage(messageEvent)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user