V1.3.1: 设备MAC改取当前网卡 + 播放界面当前/下一首实时刷新 + 日志IP与启动信息调整
1) 设备 MAC: 新增 DeviceMac 解析「当前在用网卡」硬件地址(优先 wlan/eth/en, 忽略虚拟/蜂窝/VPN; 直接读 NetworkInterface.getHardwareAddress, 规避 Android 6+ WifiInfo 恒返回 02:00:00:00:00:00 的限制), 读出无效值或失败时回退历史抓包值。 结果按进程缓存, 网络切换时由 remoteNetworkMonitor 重解析。 消费方: KtvJsBridge.DEVICE_MAC(JS 端 mac)、whoami 心跳 _mac/androidid/_cid。 2) 播放界面: 抽出 refreshNowPlayingLabels() 作为「当前播放/下一首」的唯一刷新入口, 只在文本变化时写 View, 并挂到 lyricTicker(500ms), 使播放界面左上角、首页播放条、 旧版播放区与顶部状态栏在本地点歌/扫码点歌/自动切歌后都能实时同步; 顶部状态栏的「下首」改为按当前曲目位置取其后一首(原先固定取队列第 2 首)。 3) 远程日志: serverip 改为主机 IP(公网出口 IP, 查询后缓存 5 分钟、失败 1 分钟后重试, 后台线程执行), 新增 clientip 字段承载设备网卡获取到的局域网 IP; 启动上报改为 采集 10 项: 包名、版本号、安装时间、品牌、型号、设备名、Android 版本、CPU 架构、 内核版本、屏幕分辨率。 4) 版本号 1.3.0 -> 1.3.1(versionCode 6 -> 7)。本次不发布 Release, 仅本地打包测试。
This commit is contained in:
@@ -23,7 +23,8 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
* date 日期时间,格式 `yyyy-MM-dd HH:mm:ss.SSS`(务必与检索格式一致)
|
||||
* text 日志内容
|
||||
* servername 主机名(设备型号)
|
||||
* serverip 主机 IP(设备局域网 IP)
|
||||
* serverip 主机 IP —— 公网出口 IP,由 [PUBLIC_IP_URL] 查询,结果缓存 [PUBLIC_IP_TTL_MS]
|
||||
* clientip 客户端 IP —— 设备当前网卡获取到的局域网 IP(由调用方注入)
|
||||
* loglevel 日志级别(INFO / WARN / ERROR)
|
||||
* traceid 追踪码(此处用设备标识)
|
||||
* user 用户(此处用设备标识)
|
||||
@@ -35,6 +36,16 @@ object MiliLog {
|
||||
private const val TAG = "MiliLog"
|
||||
private const val ENDPOINT = "http://log.kasugano.cn/glc/v1/log/add"
|
||||
|
||||
/** 公网 IP 查询接口(主机 IP)。 */
|
||||
private const val PUBLIC_IP_URL = "https://ipinfo.io/ip"
|
||||
|
||||
/** 公网 IP 缓存时长:5 分钟内复用,避免频繁调用外部接口。 */
|
||||
private const val PUBLIC_IP_TTL_MS = 5 * 60 * 1000L
|
||||
|
||||
/** 公网 IP 查询失败后的重试间隔。 */
|
||||
private const val PUBLIC_IP_RETRY_MS = 60 * 1000L
|
||||
private const val PUBLIC_IP_TIMEOUT_MS = 6000
|
||||
|
||||
/** GlogCenter 页面上的「系统名」,用于区分应用。 */
|
||||
private const val SYSTEM = "maidongktv"
|
||||
private const val MAX_TEXT_LEN = 900
|
||||
@@ -46,11 +57,18 @@ object MiliLog {
|
||||
|
||||
private val started = AtomicBoolean(false)
|
||||
private val running = AtomicBoolean(false)
|
||||
private val publicIpFetching = AtomicBoolean(false)
|
||||
private val queue = LinkedBlockingQueue<String>(MAX_QUEUE)
|
||||
private var senderThread: Thread? = null
|
||||
|
||||
@Volatile private var serverName: String = "android"
|
||||
|
||||
/** 主机 IP:公网出口 IP([PUBLIC_IP_URL] 查询,5 分钟缓存)。 */
|
||||
@Volatile private var serverIp: String = ""
|
||||
@Volatile private var serverIpAt: Long = 0L
|
||||
|
||||
/** 客户端 IP:设备当前网卡获取到的局域网 IP。 */
|
||||
@Volatile private var clientIp: String = ""
|
||||
@Volatile private var traceId: String = ""
|
||||
@Volatile private var user: String = ""
|
||||
|
||||
@@ -60,11 +78,21 @@ object MiliLog {
|
||||
serverName = sanitize(Build.MODEL).ifEmpty { "android" }
|
||||
running.set(true)
|
||||
senderThread = Thread({ sendLoop() }, "mili-glog").also { it.isDaemon = true; it.start() }
|
||||
ensureServerIp()
|
||||
}
|
||||
|
||||
/** 更新上报用的设备网络/身份字段(IP 变化或重新登录时调用)。 */
|
||||
/**
|
||||
* 更新上报用的客户端网络/身份字段。
|
||||
*
|
||||
* @param ip 设备当前网卡获取到的局域网 IP(clientip),变化时会顺带复核公网 IP
|
||||
* @param deviceId 设备标识(同时写入 user / traceid)
|
||||
*/
|
||||
fun updateDeviceContext(ip: String? = null, deviceId: String? = null) {
|
||||
if (!ip.isNullOrBlank()) serverIp = ip
|
||||
if (!ip.isNullOrBlank() && ip != clientIp) {
|
||||
clientIp = ip
|
||||
// 局域网 IP 变化通常意味着换了网络,公网出口 IP 需要立即复核。
|
||||
ensureServerIp(force = true)
|
||||
}
|
||||
if (!deviceId.isNullOrBlank()) {
|
||||
user = deviceId
|
||||
traceId = deviceId
|
||||
@@ -78,6 +106,8 @@ object MiliLog {
|
||||
|
||||
private fun enqueue(level: String, msg: String) {
|
||||
if (!started.get()) return
|
||||
// 公网 IP 过期时顺带后台刷新(内部有 5 分钟节流,不会频繁调用外部接口)。
|
||||
ensureServerIp()
|
||||
val body = buildBody(level, msg) ?: return
|
||||
// 队列满时丢最旧,保证新日志能进队。
|
||||
if (!queue.offer(body)) {
|
||||
@@ -101,6 +131,7 @@ object MiliLog {
|
||||
put("text", truncated)
|
||||
put("servername", serverName)
|
||||
put("serverip", serverIp)
|
||||
put("clientip", clientIp)
|
||||
put("loglevel", level)
|
||||
put("traceid", traceId)
|
||||
put("user", user)
|
||||
@@ -116,6 +147,86 @@ object MiliLog {
|
||||
value.map { if (it.isLetterOrDigit() || it == '-' || it == '_' || it == ' ') it else ' ' }
|
||||
.joinToString("").trim()
|
||||
|
||||
// ─── 公网 IP(主机 IP)────────────────────────────
|
||||
|
||||
/**
|
||||
* 确保主机 IP(公网出口 IP)可用。
|
||||
*
|
||||
* 仅在「从未获取过 / 已超过 [PUBLIC_IP_TTL_MS] / 显式要求复核」时发起查询,
|
||||
* 且同一时刻只允许一个查询在飞。查询在独立后台线程完成,绝不阻塞调用方
|
||||
* (`enqueue` 可能来自主线程)。
|
||||
*/
|
||||
private fun ensureServerIp(force: Boolean = false) {
|
||||
val age = System.currentTimeMillis() - serverIpAt
|
||||
val fresh = serverIp.isNotEmpty() && !force && age < PUBLIC_IP_TTL_MS
|
||||
if (fresh) return
|
||||
if (!publicIpFetching.compareAndSet(false, true)) return
|
||||
Thread({
|
||||
try {
|
||||
val ip = queryPublicIp()
|
||||
if (ip.isNotEmpty()) {
|
||||
serverIp = ip
|
||||
serverIpAt = System.currentTimeMillis()
|
||||
} else {
|
||||
// 查询失败:保留旧值,但延后重试,避免每条日志都触发一次请求。
|
||||
serverIpAt = System.currentTimeMillis() - PUBLIC_IP_TTL_MS + PUBLIC_IP_RETRY_MS
|
||||
}
|
||||
} finally {
|
||||
publicIpFetching.set(false)
|
||||
}
|
||||
}, "mili-pubip").also { it.isDaemon = true; it.start() }
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待主机 IP 就绪(最多 [timeoutMs]),供启动上报使用。
|
||||
*
|
||||
* 调用方必须在子线程(内部会短暂 sleep);超时未就绪则返回空串,不影响上报。
|
||||
*/
|
||||
fun awaitServerIp(timeoutMs: Long = 3000L): String {
|
||||
ensureServerIp()
|
||||
val deadline = System.currentTimeMillis() + timeoutMs
|
||||
while (serverIp.isEmpty() && System.currentTimeMillis() < deadline) {
|
||||
try {
|
||||
Thread.sleep(100L)
|
||||
} catch (e: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
break
|
||||
}
|
||||
}
|
||||
return serverIp
|
||||
}
|
||||
|
||||
/** 查询公网出口 IP([PUBLIC_IP_URL] 返回纯文本 IP);失败返回空串。 */
|
||||
private fun queryPublicIp(): String = try {
|
||||
val connection = (URL(PUBLIC_IP_URL).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
connectTimeout = PUBLIC_IP_TIMEOUT_MS
|
||||
readTimeout = PUBLIC_IP_TIMEOUT_MS
|
||||
setRequestProperty("User-Agent", "MaidongKTV/${BuildConfig.VERSION_NAME}")
|
||||
}
|
||||
try {
|
||||
val code = connection.responseCode
|
||||
if (code in 200..299) {
|
||||
val text = connection.inputStream.use { it.readBytes().toString(Charsets.UTF_8) }.trim()
|
||||
// 只接受形如 IPv4/IPv6 的纯文本,长度上限 45(IPv6 最长)。
|
||||
if (text.isNotEmpty() && text.length <= 45 &&
|
||||
text.all { it.isLetterOrDigit() || it == '.' || it == ':' }
|
||||
) {
|
||||
text
|
||||
} else {
|
||||
""
|
||||
}
|
||||
} else {
|
||||
""
|
||||
}
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "public ip query failed: ${e.message}")
|
||||
""
|
||||
}
|
||||
|
||||
// ─── 发送线程 ────────────────────────────────────
|
||||
|
||||
private fun sendLoop() {
|
||||
|
||||
Reference in New Issue
Block a user