mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Fix Android app crash on WebSocket connection loss
Every Socket.IO listener now goes through a safeOn helper that wraps the body in try/catch(Throwable). Unsafe args[0] as JSONObject and data.getString() patterns replaced with firstOrNull as? JSONObject and optString — a malformed payload from the server, or a transient state error during disconnect, no longer surfaces as an unhandled exception on the IO thread. Reconnection now uses explicit exponential backoff with jitter (1s → 60s, randomizationFactor 0.5) so a fleet doesn't reconnect in lockstep after a server blip. EVENT_DISCONNECT stops the heartbeat while disconnected; the player keeps showing cached content. register, sendHeartbeat, requestPlaylistRefresh, sendScreenshot, sendContentAck, sendPlaybackState, and disconnect are all wrapped — telemetry / WiFi service calls can throw on some devices. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cd6e39a4a7
commit
8866e305f0
|
|
@ -65,6 +65,20 @@ class WebSocketService : Service() {
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap every Socket.IO listener body in try/catch. A malformed payload from the server
|
||||||
|
// (or a transient state error during disconnect) used to surface as an unhandled
|
||||||
|
// exception on the Socket.IO IO thread and crash the whole app.
|
||||||
|
private fun Socket.safeOn(event: String, handler: (Array<Any?>) -> Unit): Socket {
|
||||||
|
return on(event) { args ->
|
||||||
|
try {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
handler(args as Array<Any?>)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e("WebSocketService", "Listener for '$event' failed: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun connect(serverUrl: String? = null) {
|
fun connect(serverUrl: String? = null) {
|
||||||
val url = serverUrl ?: config.serverUrl
|
val url = serverUrl ?: config.serverUrl
|
||||||
if (url.isEmpty()) {
|
if (url.isEmpty()) {
|
||||||
|
|
@ -79,189 +93,206 @@ class WebSocketService : Service() {
|
||||||
forceNew = true
|
forceNew = true
|
||||||
reconnection = true
|
reconnection = true
|
||||||
reconnectionAttempts = Integer.MAX_VALUE
|
reconnectionAttempts = Integer.MAX_VALUE
|
||||||
reconnectionDelay = 2000
|
// Exponential backoff: starts at 1s, doubles each attempt, capped at 60s,
|
||||||
reconnectionDelayMax = 10000
|
// ±50% jitter so a fleet doesn't reconnect in lockstep after a server blip.
|
||||||
|
reconnectionDelay = 1000
|
||||||
|
reconnectionDelayMax = 60_000
|
||||||
|
randomizationFactor = 0.5
|
||||||
timeout = 20000
|
timeout = 20000
|
||||||
}
|
}
|
||||||
|
|
||||||
socket = IO.socket(URI.create("$url/device"), options).apply {
|
socket = IO.socket(URI.create("$url/device"), options).apply {
|
||||||
on(Socket.EVENT_CONNECT) {
|
safeOn(Socket.EVENT_CONNECT) {
|
||||||
Log.i("WebSocketService", "Connected to server")
|
Log.i("WebSocketService", "Connected to server")
|
||||||
register()
|
register()
|
||||||
}
|
}
|
||||||
|
|
||||||
on(Socket.EVENT_DISCONNECT) {
|
safeOn(Socket.EVENT_DISCONNECT) { args ->
|
||||||
Log.w("WebSocketService", "Disconnected from server")
|
val reason = args.firstOrNull()?.toString() ?: "unknown"
|
||||||
|
Log.w("WebSocketService", "Disconnected from server: $reason")
|
||||||
|
// Stop heartbeat while disconnected; player keeps showing cached content.
|
||||||
|
// Socket.IO will reconnect automatically per the options above.
|
||||||
|
stopHeartbeat()
|
||||||
}
|
}
|
||||||
|
|
||||||
on(Socket.EVENT_CONNECT_ERROR) { args ->
|
safeOn(Socket.EVENT_CONNECT_ERROR) { args ->
|
||||||
Log.e("WebSocketService", "Connection error: ${args.firstOrNull()}")
|
Log.e("WebSocketService", "Connection error: ${args.firstOrNull()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
on("device:registered") { args ->
|
safeOn("device:registered") { args ->
|
||||||
val data = args[0] as JSONObject
|
val data = args.firstOrNull() as? JSONObject ?: return@safeOn
|
||||||
val newDeviceId = data.getString("device_id")
|
val newDeviceId = data.optString("device_id", "")
|
||||||
|
if (newDeviceId.isEmpty()) {
|
||||||
|
Log.w("WebSocketService", "device:registered missing device_id")
|
||||||
|
return@safeOn
|
||||||
|
}
|
||||||
config.deviceId = newDeviceId
|
config.deviceId = newDeviceId
|
||||||
// Persist device_token (issued on first register, or refreshed on reconnect)
|
// Persist device_token (issued on first register, or refreshed on reconnect)
|
||||||
if (data.has("device_token")) {
|
if (data.has("device_token")) {
|
||||||
config.deviceToken = data.getString("device_token")
|
config.deviceToken = data.optString("device_token", "")
|
||||||
}
|
}
|
||||||
Log.i("WebSocketService", "Registered as: $newDeviceId")
|
Log.i("WebSocketService", "Registered as: $newDeviceId")
|
||||||
handler.post { onRegistered?.invoke(newDeviceId) }
|
handler.post { try { onRegistered?.invoke(newDeviceId) } catch (e: Throwable) { Log.e("WebSocketService", "onRegistered cb: ${e.message}") } }
|
||||||
startHeartbeat()
|
startHeartbeat()
|
||||||
}
|
}
|
||||||
|
|
||||||
on("device:unpaired") {
|
safeOn("device:unpaired") {
|
||||||
Log.w("WebSocketService", "Device not found on server - clearing credentials")
|
Log.w("WebSocketService", "Device not found on server - clearing credentials")
|
||||||
config.clearDeviceCredentials()
|
config.clearDeviceCredentials()
|
||||||
handler.post { onUnpaired?.invoke() }
|
handler.post { try { onUnpaired?.invoke() } catch (e: Throwable) { Log.e("WebSocketService", "onUnpaired cb: ${e.message}") } }
|
||||||
}
|
}
|
||||||
|
|
||||||
on("device:auth-error") { args ->
|
safeOn("device:auth-error") { args ->
|
||||||
val msg = (args.firstOrNull() as? JSONObject)?.optString("error", "Authentication failed") ?: "Authentication failed"
|
val msg = (args.firstOrNull() as? JSONObject)?.optString("error", "Authentication failed") ?: "Authentication failed"
|
||||||
Log.w("WebSocketService", "Device auth rejected: $msg — clearing credentials for re-pair")
|
Log.w("WebSocketService", "Device auth rejected: $msg — clearing credentials for re-pair")
|
||||||
config.clearDeviceCredentials()
|
config.clearDeviceCredentials()
|
||||||
handler.post { onUnpaired?.invoke() }
|
handler.post { try { onUnpaired?.invoke() } catch (e: Throwable) { Log.e("WebSocketService", "onUnpaired cb: ${e.message}") } }
|
||||||
}
|
}
|
||||||
|
|
||||||
on("device:paired") { args ->
|
safeOn("device:paired") { args ->
|
||||||
val data = args[0] as JSONObject
|
val data = args.firstOrNull() as? JSONObject ?: return@safeOn
|
||||||
val id = data.getString("device_id")
|
val id = data.optString("device_id", "")
|
||||||
val name = data.optString("name", "Display")
|
val name = data.optString("name", "Display")
|
||||||
config.setPaired(true)
|
config.setPaired(true)
|
||||||
config.deviceName = name
|
config.deviceName = name
|
||||||
Log.i("WebSocketService", "Paired as: $name")
|
Log.i("WebSocketService", "Paired as: $name")
|
||||||
handler.post { onPaired?.invoke(id, name) }
|
handler.post { try { onPaired?.invoke(id, name) } catch (e: Throwable) { Log.e("WebSocketService", "onPaired cb: ${e.message}") } }
|
||||||
}
|
}
|
||||||
|
|
||||||
on("device:playlist-update") { args ->
|
safeOn("device:playlist-update") { args ->
|
||||||
Log.i("WebSocketService", "Playlist raw args: ${args.size} items, type=${args[0]?.javaClass?.name}, data=${args[0]}")
|
val data = args.firstOrNull() as? JSONObject ?: run {
|
||||||
val data = args[0] as JSONObject
|
Log.w("WebSocketService", "playlist-update with non-JSONObject payload: ${args.firstOrNull()}")
|
||||||
Log.i("WebSocketService", "Playlist update received, keys=${data.keys().asSequence().toList()}, assignments=${data.optJSONArray("assignments")?.length() ?: "null"}")
|
return@safeOn
|
||||||
handler.post { onPlaylistUpdate?.invoke(data) }
|
}
|
||||||
|
Log.i("WebSocketService", "Playlist update received, assignments=${data.optJSONArray("assignments")?.length() ?: "null"}")
|
||||||
|
handler.post { try { onPlaylistUpdate?.invoke(data) } catch (e: Throwable) { Log.e("WebSocketService", "onPlaylistUpdate cb: ${e.message}") } }
|
||||||
}
|
}
|
||||||
|
|
||||||
on("device:content-delete") { args ->
|
safeOn("device:content-delete") { args ->
|
||||||
val data = args[0] as JSONObject
|
val data = args.firstOrNull() as? JSONObject ?: return@safeOn
|
||||||
val contentId = data.getString("content_id")
|
val contentId = data.optString("content_id", "")
|
||||||
handler.post { onContentDelete?.invoke(contentId) }
|
if (contentId.isNotEmpty()) {
|
||||||
}
|
handler.post { try { onContentDelete?.invoke(contentId) } catch (e: Throwable) { Log.e("WebSocketService", "onContentDelete cb: ${e.message}") } }
|
||||||
|
|
||||||
on("device:screenshot-request") {
|
|
||||||
captureAndSendScreenshot()
|
|
||||||
handler.post { onScreenshotRequest?.invoke() }
|
|
||||||
}
|
|
||||||
|
|
||||||
on("device:remote-start") {
|
|
||||||
startScreenshotStream()
|
|
||||||
handler.post { onRemoteStart?.invoke() }
|
|
||||||
}
|
|
||||||
|
|
||||||
on("device:remote-stop") {
|
|
||||||
stopScreenshotStream()
|
|
||||||
handler.post { onRemoteStop?.invoke() }
|
|
||||||
}
|
|
||||||
|
|
||||||
on("device:remote-touch") { args ->
|
|
||||||
val data = args[0] as JSONObject
|
|
||||||
val x = data.getDouble("x").toFloat()
|
|
||||||
val y = data.getDouble("y").toFloat()
|
|
||||||
val action = data.optString("action", "tap")
|
|
||||||
// Use AccessibilityService for system-wide touch (works on dialogs too)
|
|
||||||
val svc = PowerAccessibilityService.instance
|
|
||||||
if (svc != null && action == "tap") {
|
|
||||||
handler.post { svc.injectTap(x, y) }
|
|
||||||
} else {
|
|
||||||
handler.post { onRemoteTouch?.invoke(x, y, action) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
on("device:remote-key") { args ->
|
safeOn("device:screenshot-request") {
|
||||||
val data = args[0] as JSONObject
|
captureAndSendScreenshot()
|
||||||
val keycode = data.getString("keycode")
|
handler.post { try { onScreenshotRequest?.invoke() } catch (e: Throwable) { Log.e("WebSocketService", "onScreenshotRequest cb: ${e.message}") } }
|
||||||
// Always inject via shell (works even when app not in foreground)
|
|
||||||
injectKey(keycode)
|
|
||||||
handler.post { onRemoteKey?.invoke(keycode) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
on("device:command") { args ->
|
safeOn("device:remote-start") {
|
||||||
val data = args[0] as JSONObject
|
startScreenshotStream()
|
||||||
val type = data.getString("type")
|
handler.post { try { onRemoteStart?.invoke() } catch (e: Throwable) { Log.e("WebSocketService", "onRemoteStart cb: ${e.message}") } }
|
||||||
|
}
|
||||||
|
|
||||||
|
safeOn("device:remote-stop") {
|
||||||
|
stopScreenshotStream()
|
||||||
|
handler.post { try { onRemoteStop?.invoke() } catch (e: Throwable) { Log.e("WebSocketService", "onRemoteStop cb: ${e.message}") } }
|
||||||
|
}
|
||||||
|
|
||||||
|
safeOn("device:remote-touch") { args ->
|
||||||
|
val data = args.firstOrNull() as? JSONObject ?: return@safeOn
|
||||||
|
val x = data.optDouble("x", 0.0).toFloat()
|
||||||
|
val y = data.optDouble("y", 0.0).toFloat()
|
||||||
|
val action = data.optString("action", "tap")
|
||||||
|
val svc = PowerAccessibilityService.instance
|
||||||
|
if (svc != null && action == "tap") {
|
||||||
|
handler.post { try { svc.injectTap(x, y) } catch (e: Throwable) { Log.e("WebSocketService", "injectTap: ${e.message}") } }
|
||||||
|
} else {
|
||||||
|
handler.post { try { onRemoteTouch?.invoke(x, y, action) } catch (e: Throwable) { Log.e("WebSocketService", "onRemoteTouch cb: ${e.message}") } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
safeOn("device:remote-key") { args ->
|
||||||
|
val data = args.firstOrNull() as? JSONObject ?: return@safeOn
|
||||||
|
val keycode = data.optString("keycode", "")
|
||||||
|
if (keycode.isEmpty()) return@safeOn
|
||||||
|
injectKey(keycode)
|
||||||
|
handler.post { try { onRemoteKey?.invoke(keycode) } catch (e: Throwable) { Log.e("WebSocketService", "onRemoteKey cb: ${e.message}") } }
|
||||||
|
}
|
||||||
|
|
||||||
|
safeOn("device:command") { args ->
|
||||||
|
val data = args.firstOrNull() as? JSONObject ?: return@safeOn
|
||||||
|
val type = data.optString("type", "")
|
||||||
|
if (type.isEmpty()) return@safeOn
|
||||||
val payload = data.optJSONObject("payload")
|
val payload = data.optJSONObject("payload")
|
||||||
Log.i("WebSocketService", "Command received: $type")
|
Log.i("WebSocketService", "Command received: $type")
|
||||||
|
|
||||||
// Handle system commands directly in the service
|
|
||||||
when (type) {
|
when (type) {
|
||||||
"launch" -> {
|
"launch" -> {
|
||||||
handler.post {
|
handler.post {
|
||||||
val intent = Intent(this@WebSocketService, MainActivity::class.java).apply {
|
try {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
val intent = Intent(this@WebSocketService, MainActivity::class.java).apply {
|
||||||
}
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
startActivity(intent)
|
}
|
||||||
Log.i("WebSocketService", "Launched MainActivity from service")
|
startActivity(intent)
|
||||||
|
Log.i("WebSocketService", "Launched MainActivity from service")
|
||||||
|
} catch (e: Throwable) { Log.e("WebSocketService", "launch cmd: ${e.message}") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"settings" -> {
|
"settings" -> {
|
||||||
handler.post {
|
handler.post {
|
||||||
val intent = Intent(android.provider.Settings.ACTION_SETTINGS).apply {
|
try {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
val intent = Intent(android.provider.Settings.ACTION_SETTINGS).apply {
|
||||||
}
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
startActivity(intent)
|
}
|
||||||
Log.i("WebSocketService", "Opened system settings")
|
startActivity(intent)
|
||||||
|
} catch (e: Throwable) { Log.e("WebSocketService", "settings cmd: ${e.message}") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"enable_system_capture" -> {
|
"enable_system_capture" -> {
|
||||||
// Trigger MediaProjection permission request on device
|
|
||||||
handler.post {
|
handler.post {
|
||||||
com.remotedisplay.player.ScreenCapturePermissionActivity.requestPermission(this@WebSocketService)
|
try {
|
||||||
Log.i("WebSocketService", "Requesting system capture permission")
|
com.remotedisplay.player.ScreenCapturePermissionActivity.requestPermission(this@WebSocketService)
|
||||||
|
} catch (e: Throwable) { Log.e("WebSocketService", "enable_system_capture: ${e.message}") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"screen_off" -> {
|
"screen_off" -> {
|
||||||
val a11y = PowerAccessibilityService.instance
|
val a11y = PowerAccessibilityService.instance
|
||||||
if (a11y != null) {
|
if (a11y != null) {
|
||||||
handler.post { a11y.lockScreen() }
|
handler.post { try { a11y.lockScreen() } catch (e: Throwable) { Log.e("WebSocketService", "lockScreen: ${e.message}") } }
|
||||||
} else {
|
} else {
|
||||||
Thread { try { Runtime.getRuntime().exec(arrayOf("input", "keyevent", "26")).waitFor() } catch (_: Exception) {} }.start()
|
Thread { try { Runtime.getRuntime().exec(arrayOf("input", "keyevent", "26")).waitFor() } catch (_: Exception) {} }.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"screen_on" -> {
|
"screen_on" -> {
|
||||||
// WAKEUP keyevent works from shell on most devices
|
|
||||||
Thread { try { Runtime.getRuntime().exec(arrayOf("input", "keyevent", "224")).waitFor() } catch (_: Exception) {} }.start()
|
Thread { try { Runtime.getRuntime().exec(arrayOf("input", "keyevent", "224")).waitFor() } catch (_: Exception) {} }.start()
|
||||||
}
|
}
|
||||||
else -> handler.post { onCommand?.invoke(type, payload) }
|
else -> handler.post { try { onCommand?.invoke(type, payload) } catch (e: Throwable) { Log.e("WebSocketService", "onCommand cb: ${e.message}") } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connect()
|
connect()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
Log.e("WebSocketService", "Socket setup error: ${e.message}")
|
Log.e("WebSocketService", "Socket setup error: ${e.message}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun register() {
|
private fun register() {
|
||||||
val data = JSONObject().apply {
|
try {
|
||||||
if (config.isProvisioned && config.isPaired) {
|
val data = JSONObject().apply {
|
||||||
put("device_id", config.deviceId)
|
if (config.isProvisioned && config.isPaired) {
|
||||||
// Send device_token for authentication (may be empty for legacy devices)
|
put("device_id", config.deviceId)
|
||||||
val token = config.deviceToken
|
val token = config.deviceToken
|
||||||
if (token.isNotEmpty()) {
|
if (token.isNotEmpty()) {
|
||||||
put("device_token", token)
|
put("device_token", token)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val pairingCode = (100000..999999).random().toString()
|
||||||
|
put("pairing_code", pairingCode)
|
||||||
|
config.deviceId = ""
|
||||||
|
getSharedPreferences("remote_display", MODE_PRIVATE)
|
||||||
|
.edit().putString("pairing_code", pairingCode).apply()
|
||||||
}
|
}
|
||||||
} else {
|
try { put("device_info", deviceInfo.getDeviceInfo()) } catch (e: Throwable) { Log.w("WebSocketService", "device_info: ${e.message}") }
|
||||||
// Generate a pairing code if we don't have one
|
try { put("fingerprint", deviceInfo.getFingerprint()) } catch (e: Throwable) { Log.w("WebSocketService", "fingerprint: ${e.message}") }
|
||||||
val pairingCode = (100000..999999).random().toString()
|
|
||||||
put("pairing_code", pairingCode)
|
|
||||||
config.deviceId = "" // Will be set on registered event
|
|
||||||
// Store pairing code temporarily
|
|
||||||
getSharedPreferences("remote_display", MODE_PRIVATE)
|
|
||||||
.edit().putString("pairing_code", pairingCode).apply()
|
|
||||||
}
|
}
|
||||||
put("device_info", deviceInfo.getDeviceInfo())
|
socket?.emit("device:register", data)
|
||||||
put("fingerprint", deviceInfo.getFingerprint())
|
} catch (e: Throwable) {
|
||||||
|
Log.e("WebSocketService", "register failed: ${e.message}", e)
|
||||||
}
|
}
|
||||||
socket?.emit("device:register", data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPairingCode(): String {
|
fun getPairingCode(): String {
|
||||||
|
|
@ -291,16 +322,17 @@ class WebSocketService : Service() {
|
||||||
fun requestPlaylistRefresh() {
|
fun requestPlaylistRefresh() {
|
||||||
if (socket?.connected() != true || config.deviceId.isEmpty()) return
|
if (socket?.connected() != true || config.deviceId.isEmpty()) return
|
||||||
Log.i("WebSocketService", "Requesting playlist refresh")
|
Log.i("WebSocketService", "Requesting playlist refresh")
|
||||||
// Re-register triggers the server to send current playlist
|
try {
|
||||||
val data = org.json.JSONObject().apply {
|
val data = org.json.JSONObject().apply {
|
||||||
put("device_id", config.deviceId)
|
put("device_id", config.deviceId)
|
||||||
val token = config.deviceToken
|
val token = config.deviceToken
|
||||||
if (token.isNotEmpty()) {
|
if (token.isNotEmpty()) put("device_token", token)
|
||||||
put("device_token", token)
|
try { put("device_info", deviceInfo.getDeviceInfo()) } catch (e: Throwable) { Log.w("WebSocketService", "device_info: ${e.message}") }
|
||||||
}
|
}
|
||||||
put("device_info", deviceInfo.getDeviceInfo())
|
socket?.emit("device:register", data)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e("WebSocketService", "requestPlaylistRefresh failed: ${e.message}")
|
||||||
}
|
}
|
||||||
socket?.emit("device:register", data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopHeartbeat() {
|
private fun stopHeartbeat() {
|
||||||
|
|
@ -310,11 +342,15 @@ class WebSocketService : Service() {
|
||||||
|
|
||||||
private fun sendHeartbeat() {
|
private fun sendHeartbeat() {
|
||||||
if (socket?.connected() != true) return
|
if (socket?.connected() != true) return
|
||||||
val data = JSONObject().apply {
|
try {
|
||||||
put("device_id", config.deviceId)
|
val data = JSONObject().apply {
|
||||||
put("telemetry", deviceInfo.getTelemetry())
|
put("device_id", config.deviceId)
|
||||||
|
try { put("telemetry", deviceInfo.getTelemetry()) } catch (e: Throwable) { Log.w("WebSocketService", "telemetry: ${e.message}") }
|
||||||
|
}
|
||||||
|
socket?.emit("device:heartbeat", data)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e("WebSocketService", "sendHeartbeat failed: ${e.message}")
|
||||||
}
|
}
|
||||||
socket?.emit("device:heartbeat", data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Screenshot streaming from the service (works even when activity is paused)
|
// Screenshot streaming from the service (works even when activity is paused)
|
||||||
|
|
@ -381,11 +417,13 @@ class WebSocketService : Service() {
|
||||||
|
|
||||||
fun sendScreenshot(imageBase64: String) {
|
fun sendScreenshot(imageBase64: String) {
|
||||||
if (socket?.connected() != true) return
|
if (socket?.connected() != true) return
|
||||||
val data = JSONObject().apply {
|
try {
|
||||||
put("device_id", config.deviceId)
|
val data = JSONObject().apply {
|
||||||
put("image_b64", imageBase64)
|
put("device_id", config.deviceId)
|
||||||
}
|
put("image_b64", imageBase64)
|
||||||
socket?.emit("device:screenshot", data)
|
}
|
||||||
|
socket?.emit("device:screenshot", data)
|
||||||
|
} catch (e: Throwable) { Log.w("WebSocketService", "sendScreenshot: ${e.message}") }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun injectKey(keycode: String) {
|
private fun injectKey(keycode: String) {
|
||||||
|
|
@ -440,28 +478,32 @@ class WebSocketService : Service() {
|
||||||
|
|
||||||
fun sendContentAck(contentId: String, status: String) {
|
fun sendContentAck(contentId: String, status: String) {
|
||||||
if (socket?.connected() != true) return
|
if (socket?.connected() != true) return
|
||||||
val data = JSONObject().apply {
|
try {
|
||||||
put("device_id", config.deviceId)
|
val data = JSONObject().apply {
|
||||||
put("content_id", contentId)
|
put("device_id", config.deviceId)
|
||||||
put("status", status)
|
put("content_id", contentId)
|
||||||
}
|
put("status", status)
|
||||||
socket?.emit("device:content-ack", data)
|
}
|
||||||
|
socket?.emit("device:content-ack", data)
|
||||||
|
} catch (e: Throwable) { Log.w("WebSocketService", "sendContentAck: ${e.message}") }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendPlaybackState(contentId: String, positionSec: Float) {
|
fun sendPlaybackState(contentId: String, positionSec: Float) {
|
||||||
if (socket?.connected() != true) return
|
if (socket?.connected() != true) return
|
||||||
val data = JSONObject().apply {
|
try {
|
||||||
put("device_id", config.deviceId)
|
val data = JSONObject().apply {
|
||||||
put("current_content_id", contentId)
|
put("device_id", config.deviceId)
|
||||||
put("position_sec", positionSec)
|
put("current_content_id", contentId)
|
||||||
}
|
put("position_sec", positionSec)
|
||||||
socket?.emit("device:playback-state", data)
|
}
|
||||||
|
socket?.emit("device:playback-state", data)
|
||||||
|
} catch (e: Throwable) { Log.w("WebSocketService", "sendPlaybackState: ${e.message}") }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
stopHeartbeat()
|
stopHeartbeat()
|
||||||
socket?.disconnect()
|
try { socket?.disconnect() } catch (e: Throwable) { Log.w("WebSocketService", "disconnect: ${e.message}") }
|
||||||
socket?.off()
|
try { socket?.off() } catch (e: Throwable) { Log.w("WebSocketService", "off: ${e.message}") }
|
||||||
socket = null
|
socket = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue