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)