Files
flynn/apps/companion/android/AndroidCompanionRuntime.kt
T
2026-02-26 20:56:43 -08:00

123 lines
3.4 KiB
Kotlin

package flynn.companion
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import org.json.JSONArray
import org.json.JSONObject
interface CompanionTransport {
fun connect(url: String, token: String?)
fun disconnect()
fun send(rawMessage: String)
}
data class CompanionStatusPayload(
val platform: String,
val appVersion: String? = null,
val statusText: String? = null,
val batteryPct: Double? = null
)
data class CompanionLocationPayload(
val latitude: Double,
val longitude: Double,
val source: String = "device"
)
class AndroidCompanionRuntime(
private val bootstrap: CompanionBootstrap,
private val transport: CompanionTransport
) {
private var nextRequestId: Int = 1
private val scheduler = Executors.newSingleThreadScheduledExecutor()
private var heartbeatFuture: ScheduledFuture<*>? = null
fun connect() {
transport.connect(bootstrap.gateway.url, bootstrap.gateway.token)
}
fun disconnect() {
heartbeatFuture?.cancel(true)
heartbeatFuture = null
transport.disconnect()
}
fun registerNode() {
val params = JSONObject()
.put("nodeId", bootstrap.node.nodeId)
.put("role", bootstrap.node.role)
.put("platform", bootstrap.node.platform)
.put("capabilities", JSONArray(bootstrap.node.capabilities))
sendRpc("node.register", params)
}
fun publishStatus(status: CompanionStatusPayload) {
val statusJson = JSONObject()
.put("platform", status.platform)
status.appVersion?.let { statusJson.put("appVersion", it) }
status.statusText?.let { statusJson.put("statusText", it) }
status.batteryPct?.let { statusJson.put("batteryPct", it) }
sendRpc(
"node.status.set",
JSONObject()
.put("nodeId", bootstrap.node.nodeId)
.put("status", statusJson)
)
}
fun publishLocation(location: CompanionLocationPayload) {
sendRpc(
"node.location.set",
JSONObject()
.put("nodeId", bootstrap.node.nodeId)
.put(
"location",
JSONObject()
.put("latitude", location.latitude)
.put("longitude", location.longitude)
.put("source", location.source)
)
)
}
fun registerPushToken(token: String, topic: String? = null) {
val params = JSONObject()
.put("nodeId", bootstrap.node.nodeId)
.put("provider", "fcm")
.put("token", token)
topic?.let { params.put("topic", it) }
sendRpc("node.push_token.set", params)
}
fun sendHandoff(message: String, sessionId: String? = null) {
val params = JSONObject()
.put("message", message)
.put("awaitResponse", true)
sessionId?.let { params.put("sessionId", it) }
sendRpc("agent.send", params)
}
fun startHeartbeatLoop(statusFactory: () -> CompanionStatusPayload) {
heartbeatFuture?.cancel(true)
val intervalSeconds = bootstrap.runtime.heartbeatSeconds.coerceAtLeast(1).toLong()
heartbeatFuture = scheduler.scheduleAtFixedRate(
{ publishStatus(statusFactory()) },
0L,
intervalSeconds,
TimeUnit.SECONDS
)
}
private fun sendRpc(method: String, params: JSONObject) {
val request = JSONObject()
.put("jsonrpc", "2.0")
.put("id", nextRequestId++)
.put("method", method)
.put("params", params)
transport.send(request.toString())
}
}
// Generated for node: android-reference-shell (android)