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, 仅本地打包测试。
109 lines
4.6 KiB
Kotlin
109 lines
4.6 KiB
Kotlin
package com.local.ktv
|
||
|
||
import android.util.Log
|
||
import java.net.NetworkInterface
|
||
import java.util.Locale
|
||
|
||
/**
|
||
* 设备 MAC 解析 —— 取「当前实际在用的网卡」的硬件地址。
|
||
*
|
||
* 背景:歌曲源(w.w345.my / e.ac19.cn 等)与设备心跳(whoami)按 `device`(mac + sn)
|
||
* 识别设备。早期实现把 mac 写死为抓包设备的固定值,导致所有设备共用同一身份。
|
||
* 这里改为读取本机当前启用网卡的 MAC(12 位小写十六进制、无分隔符)。
|
||
*
|
||
* 取值优先级:
|
||
* 1) 启用中、非虚拟、名称形如 `wlan*` / `eth*` / `en*` 的网卡(有线优先于无线);
|
||
* 2) 其它启用中的非虚拟网卡;
|
||
* 3) [FALLBACK_MAC](历史抓包值,保证读不到时仍沿用旧身份,不会变成空值)。
|
||
*
|
||
* 注意:
|
||
* - Android 6 起 `WifiInfo.getMacAddress()` 恒返回 `02:00:00:00:00:00`,
|
||
* 因此这里不使用 WifiManager,直接读 [NetworkInterface.getHardwareAddress];
|
||
* - `NetworkInterface.isUp/isVirtual` 是 API 24 才有的方法,低版本上会抛错,
|
||
* 统一用 [safeUp]/[safeVirtual] 包一层,失败时按「可用/非虚拟」处理;
|
||
* - 结果按进程缓存,避免每次点歌/心跳都枚举网卡;网络切换时调用 [refresh] 重新解析。
|
||
*/
|
||
object DeviceMac {
|
||
private const val TAG = "DeviceMac"
|
||
|
||
/** 历史抓包设备 MAC:读不到真实网卡时兜底,保证设备身份始终非空。 */
|
||
const val FALLBACK_MAC = "ae60e9547958"
|
||
|
||
private const val HEX = "0123456789abcdef"
|
||
|
||
@Volatile private var cached: String? = null
|
||
|
||
/** 当前网卡 MAC(小写、无分隔符);读不到返回空串。 */
|
||
fun current(): String {
|
||
cached?.let { return it }
|
||
synchronized(this) {
|
||
cached?.let { return it }
|
||
val value = read()
|
||
cached = value
|
||
if (value.isEmpty()) Log.w(TAG, "no usable network interface mac, fallback to legacy id")
|
||
return value
|
||
}
|
||
}
|
||
|
||
/** 当前网卡 MAC;读不到时回退到 [FALLBACK_MAC]。 */
|
||
fun effective(): String = current().ifEmpty { FALLBACK_MAC }
|
||
|
||
/** 清空缓存并重新解析(网络切换 / 网卡热插拔后调用)。 */
|
||
fun refresh(): String {
|
||
synchronized(this) { cached = null }
|
||
return current()
|
||
}
|
||
|
||
private fun read(): String {
|
||
var candidate = ""
|
||
try {
|
||
val interfaces = NetworkInterface.getNetworkInterfaces() ?: return ""
|
||
while (interfaces.hasMoreElements()) {
|
||
val nif = interfaces.nextElement() ?: continue
|
||
if (!safeUp(nif) || safeLoopback(nif) || safeVirtual(nif)) continue
|
||
val name = nif.name.orEmpty().lowercase(Locale.ROOT)
|
||
if (isIgnoredName(name)) continue
|
||
val hex = nif.hardwareAddress.toMacHex()
|
||
if (hex.isEmpty()) continue
|
||
// 有线/无线网卡直接采用;否则先记下,继续找更合适的。
|
||
if (isPreferredName(name)) return hex
|
||
if (candidate.isEmpty()) candidate = hex
|
||
}
|
||
} catch (e: Throwable) {
|
||
Log.w(TAG, "read network mac failed: ${e.message}")
|
||
}
|
||
return candidate
|
||
}
|
||
|
||
/** 虚拟/点对点/蜂窝接口不算「当前使用的网卡」。 */
|
||
private fun isIgnoredName(name: String): Boolean =
|
||
name.startsWith("tun") || name.startsWith("ppp") || name.startsWith("wg") ||
|
||
name.startsWith("rmnet") || name.startsWith("radio") || name.startsWith("ccmni") ||
|
||
name.startsWith("pdp") || name.startsWith("dummy") || name.startsWith("sit") ||
|
||
name.startsWith("ip6tnl") || name.startsWith("gre") || name.contains("vpn")
|
||
|
||
private fun isPreferredName(name: String): Boolean =
|
||
name.startsWith("wlan") || name.startsWith("eth") || name.startsWith("en")
|
||
|
||
private fun ByteArray?.toMacHex(): String {
|
||
if (this == null || size < 6) return ""
|
||
val sb = StringBuilder(size * 2)
|
||
for (b in this) {
|
||
val v = b.toInt() and 0xFF
|
||
sb.append(HEX[v shr 4]).append(HEX[v and 0x0F])
|
||
}
|
||
val hex = sb.toString()
|
||
// 全 0 与 Android 6+ 的占位 MAC 视为无效。
|
||
return if (hex == "000000000000" || hex == "020000000000") "" else hex
|
||
}
|
||
|
||
private fun safeUp(nif: NetworkInterface): Boolean =
|
||
try { nif.isUp } catch (e: Throwable) { true }
|
||
|
||
private fun safeLoopback(nif: NetworkInterface): Boolean =
|
||
try { nif.isLoopback } catch (e: Throwable) { false }
|
||
|
||
private fun safeVirtual(nif: NetworkInterface): Boolean =
|
||
try { nif.isVirtual } catch (e: Throwable) { false }
|
||
}
|