Files
Matewave/app/src/main/java/cc/skysparkle/matewave/share/ApkShareServer.kt
T
Claude 6ac5a582db Add app source for F-Droid release (v1.0.43)
Adds the full Android source tree with unit tests, fastlane store
metadata and the Gradle wrapper. The archive shipped without
gradle-wrapper.jar, so the official Gradle 9.7.1 wrapper jar is
included to make ./gradlew work from a fresh clone. .gitignore now
also excludes build output and keystores. README gains build
instructions and an updated structure tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015g6ECVSKcBG8CJPrqC3G5o
2026-10-06 18:23:20 +00:00

129 lines
5.0 KiB
Kotlin

package cc.skysparkle.matewave.share
import java.io.BufferedOutputStream
import java.io.File
import java.io.IOException
import java.io.OutputStream
import java.net.Inet4Address
import java.net.NetworkInterface
import java.net.ServerSocket
import java.net.Socket
import java.util.Locale
import kotlin.concurrent.thread
class ApkShareServer(private val apk: File, private val appName: String) {
private val fileName = "$appName.apk"
@Volatile
private var server: ServerSocket? = null
fun start(): Int {
val socket = ServerSocket(0)
server = socket
thread(name = "apk-share-accept", isDaemon = true) {
while (!socket.isClosed) {
val client = try {
socket.accept()
} catch (e: IOException) {
break
}
thread(name = "apk-share-client", isDaemon = true) { serve(client) }
}
}
return socket.localPort
}
fun stop() {
runCatching { server?.close() }
server = null
}
private fun serve(client: Socket) {
try {
client.use { s ->
s.soTimeout = 15_000
val input = s.getInputStream().bufferedReader(Charsets.ISO_8859_1)
val requestLine = input.readLine() ?: return
while (true) {
val line = input.readLine() ?: break
if (line.isEmpty()) break
}
val parts = requestLine.split(' ')
val method = parts.getOrNull(0).orEmpty()
val path = parts.getOrNull(1).orEmpty().substringBefore('?')
val out = BufferedOutputStream(s.getOutputStream())
val withBody = method == "GET"
when {
method != "GET" && method != "HEAD" ->
sendText(out, 405, "Method Not Allowed", "Method Not Allowed", withBody)
path == "/" || path == "/index.html" -> {
val page = landingPage()
writeHeader(out, 200, "OK", "text/html; charset=utf-8", page.size.toLong())
if (withBody) out.write(page)
}
path == "/$fileName" -> {
writeHeader(
out, 200, "OK", "application/vnd.android.package-archive", apk.length(),
"Content-Disposition: attachment; filename=\"$fileName\"\r\n"
)
if (withBody) apk.inputStream().use { it.copyTo(out, 64 * 1024) }
}
else -> sendText(out, 404, "Not Found", "Not Found", withBody)
}
out.flush()
}
} catch (e: IOException) {
}
}
private fun sendText(out: OutputStream, code: Int, reason: String, text: String, withBody: Boolean) {
val body = text.toByteArray(Charsets.UTF_8)
writeHeader(out, code, reason, "text/plain; charset=utf-8", body.size.toLong())
if (withBody) out.write(body)
}
private fun writeHeader(
out: OutputStream, code: Int, reason: String, type: String, length: Long, extra: String = ""
) {
val header = "HTTP/1.1 $code $reason\r\n" +
"Content-Type: $type\r\n" +
"Content-Length: $length\r\n" +
"Cache-Control: no-store\r\n" +
"Connection: close\r\n" +
extra +
"\r\n"
out.write(header.toByteArray(Charsets.ISO_8859_1))
}
private fun landingPage(): ByteArray {
val size = String.format(Locale.US, "%.1f MB", apk.length() / 1_048_576.0)
val html = "<!doctype html><html><head><meta charset=\"utf-8\">" +
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">" +
"<title>$appName</title><style>" +
"body{font-family:sans-serif;background:#12181A;color:#fff;text-align:center;padding:48px 20px}" +
"a.b{display:inline-block;margin-top:28px;padding:16px 28px;border-radius:12px;" +
"background:#3E7C4A;color:#fff;font-size:18px;text-decoration:none}" +
"p{color:#b8c2c0}</style></head><body><h1>$appName</h1>" +
"<p>Android app \u00B7 APK $size</p>" +
"<a class=\"b\" href=\"/$fileName\">Download APK</a>" +
"<p>After downloading, open the file and allow installation from this source if asked.</p>" +
"</body></html>"
return html.toByteArray(Charsets.UTF_8)
}
}
object LocalNetwork {
private val WIFI_PREFIXES = listOf("wlan", "swlan", "ap", "softap", "eth")
fun wifiIpv4(): String? = runCatching {
NetworkInterface.getNetworkInterfaces()?.toList().orEmpty()
.filter { nif -> nif.isUp && !nif.isLoopback && WIFI_PREFIXES.any { nif.name.startsWith(it) } }
.flatMap { nif -> nif.inetAddresses.toList() }
.filterIsInstance<Inet4Address>()
.firstOrNull { it.isSiteLocalAddress }
?.hostAddress
}.getOrNull()
}