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
This commit is contained in:
Claude
2026-10-06 18:23:20 +00:00
parent 6851ba52ef
commit 6ac5a582db
242 changed files with 54260 additions and 1 deletions
@@ -0,0 +1,128 @@
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()
}