Pith - wedge_android
wedge_android/app/src/main/java/com/vgmlr/wedge/WedgeNetworkManager.kt [7.7 kb]
Modified: 18:56:46 154 026 (20 Aug 026)
3 Days Ago
package com.vgmlr.wedge

import kotlinx.coroutines.*
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.ServerSocket
import java.net.Socket
import org.json.JSONObject
import java.net.InetSocketAddress

class PeerManager(
    private val getLocalPassKey: () -> String,
    private val getLocalLastModified: suspend () -> Long,
    private val getAutoMerge: () -> Boolean,
    private val onPeerMeta: (Long) -> Unit,
    private val onMessageReceived: (String, Long) -> Unit,
    private val onLinkReady: () -> Unit,
    private val onStatusChanged: (String, Boolean) -> Unit
) {
    private var serverSocket: ServerSocket? = null
    @Volatile private var clientSocket: Socket? = null
    @Volatile private var writer: PrintWriter? = null
    @Volatile private var metaGate: CompletableDeferred<Long>? = null
    @Volatile var peerAutoMerge: Boolean = false
        private set

    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    @Volatile private var isListening = false

    fun isLinkUp(): Boolean {
        val s = clientSocket
        return writer != null && s != null && s.isConnected && !s.isClosed
    }

    fun autoMergeReady(): Boolean = isLinkUp() && getAutoMerge() && peerAutoMerge
    
    fun refreshMeta() {
        if (!isLinkUp()) return
        scope.launch { sendMeta() }
    }

    fun startListener() {
        if (isListening) return
        isListening = true

        scope.launch {
            try {
                serverSocket = ServerSocket(35690).apply {
                    reuseAddress = true
                }

                while (isActive) {
                    val socket = serverSocket?.accept() ?: break
                    if (isLinkUp()) { socket.close(); continue }
                    attachPeer(socket)
                    handleClientStream(socket)
                }
            } catch (_: Exception) {}
        }
    }

    suspend fun dial(host: String, port: Int, timeoutMs: Long = 5000): Boolean {
        if (isLinkUp()) return true
        return withContext(Dispatchers.IO) {
            try {
                onStatusChanged("Connecting", false)
                val gate = CompletableDeferred<Long>()
                metaGate = gate
                val socket = Socket()
                socket.connect(InetSocketAddress(host, port), timeoutMs.toInt())
                attachPeer(socket)
                scope.launch { handleClientStream(socket) }
                withTimeout(timeoutMs) { gate.await() }
                true
            } catch (_: Exception) {
                onStatusChanged("No Peer", false)
                dropPeer()
                false
            } finally {
                metaGate = null
            }
        }
    }

    private suspend fun attachPeer(socket: Socket) {
        withContext(Dispatchers.IO) {
            socket.tcpNoDelay = true
            clientSocket = socket
            writer = PrintWriter(socket.getOutputStream(), true)
        }
        onStatusChanged("Connected", true)
        sendMeta()
    }

    private fun dropPeer() {
        try { clientSocket?.close() } catch (_: Exception) {}
        clientSocket = null
        writer = null
        peerAutoMerge = false
    }

    private suspend fun handleClientStream(socket: Socket) {
        withContext(Dispatchers.IO) {
            try {
                val reader = BufferedReader(InputStreamReader(socket.getInputStream(), "UTF-8"))
                var line: String? = null

                while (isActive && reader.readLine().also { line = it } != null) {
                    line?.let { base64Payload ->
                        try {
                            val decodedBytes = android.util.Base64.decode(base64Payload, android.util.Base64.NO_WRAP)
                            val jsonString = String(decodedBytes, Charsets.UTF_8)
                            val json = JSONObject(jsonString)

                            val receivedKey = json.getString("key")
                            if (receivedKey != getLocalPassKey()) {
                                withContext(Dispatchers.Main) {
                                    onStatusChanged("Pass Key Incorrect", false)
                                }
                                dropPeer()
                                return@let
                            }

                            val type = json.optString("type", "data")
                            val peerDate = json.optLong("date", 0L)
                            peerAutoMerge = json.optBoolean("auto", false)
                            if (type == "meta") {
                                withContext(Dispatchers.Main) {
                                    onPeerMeta(peerDate)
                                    onLinkReady()
                                }
                                metaGate?.complete(peerDate)
                            } else {
                                val cleanMessage = json.getString("data")
                                withContext(Dispatchers.Main) {
                                    onPeerMeta(peerDate)
                                    onMessageReceived(cleanMessage, peerDate)
                                }
                                metaGate?.complete(peerDate)
                            }
                        } catch (_: Exception) {
                        }
                    }
                }
            } catch (_: Exception) {
            } finally {
                withContext(Dispatchers.Main) {
                    onStatusChanged("Idle", false)
                }
                dropPeer()
            }
        }
    }

    private suspend fun sendMeta() {
        try {
            val json = JSONObject().apply {
                put("type", "meta")
                put("key", getLocalPassKey())
                put("date", getLocalLastModified())
                put("auto", getAutoMerge())
            }
            val bytes = json.toString().toByteArray(Charsets.UTF_8)
            val base64Encoded = android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP)
            writer?.println(base64Encoded)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    fun sendTextMessage(message: String, passKey: String, date: Long, onSuccess: () -> Unit) {
        val out = writer
        if (!isLinkUp() || out == null) {
            scope.launch(Dispatchers.Main) { onStatusChanged("Disconnected", false) }
            return
        }
        scope.launch(Dispatchers.IO) {
            try {
                val json = JSONObject().apply {
                    put("type", "data")
                    put("key", passKey)
                    put("date", date)
                    put("auto", getAutoMerge())
                    put("data", message)
                }

                val bytes = json.toString().toByteArray(Charsets.UTF_8)
                val base64Encoded = android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP)
                out.println(base64Encoded)
                if (out.checkError()) {
                    withContext(Dispatchers.Main) { onStatusChanged("Disconnected", false) }
                    return@launch
                }
                withContext(Dispatchers.Main) {
                    onPeerMeta(date)
                    onSuccess()
                }
            } catch (e: Exception) {
                e.printStackTrace()
                withContext(Dispatchers.Main) { onStatusChanged("Disconnected", false) }
            }
        }
    }

    fun stop() {
        isListening = false
        scope.launch(Dispatchers.IO) {
            try {
                dropPeer()
                serverSocket?.close()
            } catch (e: Exception) {
                e.printStackTrace()
            }
            serverSocket = null
        }
    }
}
Updates
Kerf - Android 157.026
Kiln - Android 157.026
Wedge - Android 156.026
Whittle - Linux 155.026
Grit - Firefox/Chrome 154.026

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)
0.00285
257,065 (+319)