Skip to content

Commit

Permalink
🎨 跟换nanohttpd,设置页面新增上传apk
Browse files Browse the repository at this point in the history
  • Loading branch information
yaoxieyoulei committed Apr 22, 2024
1 parent 9f0870e commit f42bf88
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 128 deletions.
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ dependencies {

// 网络请求
implementation(libs.okhttp)
implementation(libs.nanohttpd)
implementation(libs.androidasync)


// 二维码
implementation(libs.qrose)
Expand Down
240 changes: 122 additions & 118 deletions app/src/main/java/top/yogiczy/mytv/ui/utils/HttpServer.kt
Original file line number Diff line number Diff line change
@@ -1,32 +1,56 @@
package top.yogiczy.mytv.ui.utils

import android.annotation.SuppressLint
import android.content.Context
import android.widget.Toast
import fi.iki.elonen.NanoHTTPD
import com.koushikdutta.async.AsyncServer
import com.koushikdutta.async.http.body.JSONObjectBody
import com.koushikdutta.async.http.body.MultipartFormDataBody
import com.koushikdutta.async.http.server.AsyncHttpServer
import com.koushikdutta.async.http.server.AsyncHttpServerRequest
import com.koushikdutta.async.http.server.AsyncHttpServerResponse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import top.yogiczy.mytv.AppGlobal
import top.yogiczy.mytv.R
import top.yogiczy.mytv.ui.screens.toast.ToastState
import java.io.File
import java.net.Inet4Address
import java.net.NetworkInterface
import java.net.SocketException


object HttpServer : Loggable() {
const val SERVER_PORT = 10481

private lateinit var server: NanoHTTPD
private val uploadedApkFile = File(AppGlobal.cacheDir, "uploaded_apk.apk").apply {
deleteOnExit()
}

fun start(context: Context) {
if (this::server.isInitialized) return

CoroutineScope(Dispatchers.IO).launch {
try {
server = ServerApplication(context, SERVER_PORT)
val server = AsyncHttpServer()
server.listen(AsyncServer.getDefault(), SERVER_PORT)

server.get("/") { _, response ->
handleHomePage(response, context)
}

server.get("/api/settings") { _, response ->
handleGetSettings(response)
}

server.post("/api/settings") { request, response ->
handleSetSettings(request, response)
}

server.post("/api/upload/apk") { request, response ->
handleUploadApk(request, response, context)
}

log.i("服务已启动: 0.0.0.0:${SERVER_PORT}")
} catch (ex: Exception) {
log.e("服务启动失败: ${ex.message}", ex.cause)
Expand All @@ -37,6 +61,97 @@ object HttpServer : Loggable() {
}
}

private fun handleHomePage(response: AsyncHttpServerResponse, context: Context) {
response.apply {
setContentType("text/html; charset=utf-8")
send(context.resources.openRawResource(R.raw.index).readBytes().decodeToString())
}
}

private fun handleGetSettings(response: AsyncHttpServerResponse) {
response.apply {
setContentType("application/json")
send(
Json.encodeToString(
AllSettings(
appBootLaunch = SP.appBootLaunch,

iptvChannelChangeFlip = SP.iptvChannelChangeFlip,
iptvSourceSimplify = SP.iptvSourceSimplify,
iptvSourceCachedAt = SP.iptvSourceCachedAt,
iptvSourceUrl = SP.iptvSourceUrl,
iptvSourceCacheTime = SP.iptvSourceCacheTime,

epgEnable = SP.epgEnable,
epgXmlCachedAt = SP.epgXmlCachedAt,
epgCachedHash = SP.epgCachedHash,
epgXmlUrl = SP.epgXmlUrl,
epgRefreshTimeThreshold = SP.epgRefreshTimeThreshold,

debugShowFps = SP.debugShowFps,
)
)
)
}
}

private fun handleSetSettings(
request: AsyncHttpServerRequest,
response: AsyncHttpServerResponse,
) {
val body = request.getBody<JSONObjectBody>().get()
SP.appBootLaunch = body.get("appBootLaunch") as Boolean

SP.iptvChannelChangeFlip = body.get("iptvChannelChangeFlip").toString().toBoolean()
SP.iptvSourceSimplify = body.get("iptvSourceSimplify").toString().toBoolean()
SP.iptvSourceCachedAt = body.get("iptvSourceCachedAt").toString().toLong()
SP.iptvSourceUrl = body.get("iptvSourceUrl").toString()
SP.iptvSourceCacheTime = body.get("iptvSourceCacheTime").toString().toLong()

SP.epgEnable = body.get("epgEnable").toString().toBoolean()
SP.epgXmlCachedAt = body.get("epgXmlCachedAt").toString().toLong()
SP.epgCachedHash = body.get("epgCachedHash").toString().toInt()
SP.epgXmlUrl = body.get("epgXmlUrl").toString()
SP.epgRefreshTimeThreshold = body.get("epgRefreshTimeThreshold").toString().toInt()

SP.debugShowFps = body.get("debugShowFps").toString().toBoolean()

response.send("success")
}

private fun handleUploadApk(
request: AsyncHttpServerRequest,
response: AsyncHttpServerResponse,
context: Context,
) {
val body = request.getBody<MultipartFormDataBody>()

val os = uploadedApkFile.outputStream()
val contentLength = request.headers["Content-Length"]?.toLong() ?: 1
var hasReceived = 0L

body.setMultipartCallback { part ->
if (part.isFile) {
body.setDataCallback { _, bb ->
val byteArray = bb.allByteArray
hasReceived += byteArray.size
ToastState.I.showToast("正在接收文件: ${(hasReceived * 100f / contentLength).toInt()}%")
os.write(byteArray)
}
}
}

body.setEndCallback {
ToastState.I.showToast("文件接收完成")
body.dataEmitter.close()
os.flush()
os.close()
ApkInstaller.installApk(context, uploadedApkFile.path)
}

response.send("success")
}

fun getLocalIpAddress(): String {
val defaultIp = "0.0.0.0"

Expand All @@ -58,117 +173,6 @@ object HttpServer : Loggable() {
return defaultIp
}
}


private class ServerApplication(private val context: Context, port: Int) : NanoHTTPD(port) {
init {
start(SOCKET_READ_TIMEOUT, false)
}

private fun isPreflightRequest(session: IHTTPSession): Boolean {
val headers = session.headers
return Method.OPTIONS == session.method && headers.contains("origin") && headers.containsKey(
"access-control-request-method"
) && headers.containsKey("access-control-request-headers")
}

private fun responseCORS(session: IHTTPSession): Response {
val resp = wrapResponse(session, newFixedLengthResponse(""))
val headers = session.headers
resp.addHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS")
val requestHeaders = headers["access-control-request-headers"]
if (requestHeaders != null) {
resp.addHeader("Access-Control-Allow-Headers", requestHeaders)
}
resp.addHeader("Access-Control-Max-Age", "0")
return resp
}

private fun wrapResponse(session: IHTTPSession, resp: Response): Response {
val headers = session.headers
resp.addHeader("Access-Control-Allow-Credentials", "true")
resp.addHeader("Access-Control-Allow-Origin", headers.getOrElse("origin") { "*" })
val requestHeaders = headers["access-control-request-headers"]
if (requestHeaders != null) {
resp.addHeader("Access-Control-Allow-Headers", requestHeaders)
}
return resp
}

private inline fun <reified T> responseJson(data: T): Response {
return newFixedLengthResponse(
Response.Status.OK, "application/json", Json.encodeToString(data)
)
}

private inline fun <reified T> parseBody(session: IHTTPSession): T {
val files = mutableMapOf<String, String>()
session.parseBody(files)
return Json.decodeFromString(files["postData"]!!)
}

@SuppressLint("ResourceType")
override fun serve(session: IHTTPSession): Response {
if (isPreflightRequest(session)) {
return responseCORS(session)
}

if (session.uri == "/") {
return newFixedLengthResponse(
context.resources.openRawResource(R.raw.index).readBytes().decodeToString(),
)
} else if (session.uri.startsWith("/api/settings")) {
if (session.method == Method.GET) {
return wrapResponse(
session,
responseJson(
AllSettings(
appBootLaunch = SP.appBootLaunch,

iptvChannelChangeFlip = SP.iptvChannelChangeFlip,
iptvSourceSimplify = SP.iptvSourceSimplify,
iptvSourceCachedAt = SP.iptvSourceCachedAt,
iptvSourceUrl = SP.iptvSourceUrl,
iptvSourceCacheTime = SP.iptvSourceCacheTime,

epgEnable = SP.epgEnable,
epgXmlCachedAt = SP.epgXmlCachedAt,
epgCachedHash = SP.epgCachedHash,
epgXmlUrl = SP.epgXmlUrl,
epgRefreshTimeThreshold = SP.epgRefreshTimeThreshold,

debugShowFps = SP.debugShowFps,
)
),
)
} else if (session.method == Method.POST) {
val data = parseBody<AllSettings>(session)
SP.appBootLaunch = data.appBootLaunch

SP.iptvChannelChangeFlip = data.iptvChannelChangeFlip
SP.iptvSourceSimplify = data.iptvSourceSimplify
SP.iptvSourceCachedAt = data.iptvSourceCachedAt
SP.iptvSourceUrl = data.iptvSourceUrl
SP.iptvSourceCacheTime = data.iptvSourceCacheTime

SP.epgEnable = data.epgEnable
SP.epgXmlCachedAt = data.epgXmlCachedAt
SP.epgCachedHash = data.epgCachedHash
SP.epgXmlUrl = data.epgXmlUrl
SP.epgRefreshTimeThreshold = data.epgRefreshTimeThreshold

SP.debugShowFps = data.debugShowFps

return wrapResponse(session, newFixedLengthResponse("success"))
}
}

return wrapResponse(
session,
newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Not Found")
)
}
}
}

@Serializable
Expand Down
47 changes: 40 additions & 7 deletions app/src/main/res/raw/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,14 @@
v-model="settings.debugShowFps" />
</template>
</van-cell>

<van-cell title="上传apk">
<template #extra>
<van-uploader
:after-read="uploaderAfterRead"
accept=".apk" />
</template>
</van-cell>
</van-cell-group>
</template>
</div>
Expand All @@ -246,6 +254,15 @@

const baseUrl = ""

async function requestApi(url, config) {
const resp = await fetch(`${baseUrl}${url}`, config)
if (resp.status !== 200) {
throw new Error(`请求失败:${resp.status}`)
}

return resp
}

dayjs.locale('zh-cn')
dayjs.extend(dayjs_plugin_relativeTime)

Expand All @@ -256,25 +273,27 @@

const settings = ref()
async function confirmSettings() {
let loading = vant.Toast.loading({ message: '加载中...', forbidClick: true, duration: 0 })
vant.Toast.loading({ message: '加载中...', forbidClick: true, duration: 0 })
try {
await fetch(`${baseUrl}/api/settings`, {
await requestApi('/api/settings', {
method: "POST",
body: JSON.stringify(settings.value)
body: JSON.stringify(settings.value),
headers: { 'Content-Type': 'application/json' }
})
await refreshSettings()
loading.clear()
vant.Toast.clear()
} catch (e) {
vant.Toast.fail('无法修改设置')
console.error(e)
refreshSettings()
}
}
async function refreshSettings() {
let loading = vant.Toast.loading({ message: '加载中...', forbidClick: true, duration: 0 })
vant.Toast.loading({ message: '加载中...', forbidClick: true, duration: 0 })

try {
settings.value = await (await fetch(`${baseUrl}/api/settings`)).json()
loading.clear()
settings.value = await (await requestApi('/api/settings')).json()
vant.Toast.clear()
} catch (e) {
vant.Toast.fail('无法获取设置')
console.error(e)
Expand All @@ -290,12 +309,26 @@
return humanizeDuration(ms, { language: 'zh_CN', round: true, largest: 2 })
}

async function uploaderAfterRead(file) {
vant.Toast.loading({ message: '加载中...', forbidClick: true, duration: 0 })
try {
const formData = new FormData()
formData.append('filename', file.file)
await requestApi('/api/upload/apk', { method: "POST", body: formData })
vant.Toast.clear()
} catch (e) {
vant.Toast.fail('上传apk失败')
console.error(e)
}
}

return {
dayjs,
foramtDuration,
tabActive,
settings,
confirmSettings,
uploaderAfterRead,
}
}
})
Expand Down
Loading

0 comments on commit f42bf88

Please sign in to comment.