Files
tv/app/src/main/java/com/tv/browser/NetworkChecker.kt
T
2026-11-09 15:49:32 +08:00

147 lines
6.1 KiB
Kotlin

package com.tv.browser
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import java.net.HttpURLConnection
import java.net.Inet4Address
import java.net.NetworkInterface
import java.net.URL
import java.util.Calendar
import kotlin.math.abs
/**
* 检查网络连接状态、系统验证结果、互联网实际可达性与系统时钟偏差。
*/
object NetworkChecker {
private const val INTERNET_CHECK_URL = "https://www.baidu.com"
private const val INTERNET_CHECK_TIMEOUT_MS = 8000
/** 允许的设备时钟与服务器 Date 响应头最大偏差(7 天),超过则判定为电视时钟未同步 */
private const val MAX_CLOCK_SKEW_MS = 7L * 24 * 3600 * 1000L
/** 最低合理年份基线(仅在尚未收到任何 HTTP Date 响应头时兜底) */
private const val MIN_VALID_YEAR = 2026
@Volatile
private var lastObservedSkewMs: Long? = null
/** 根据服务器返回的 Date 头毫秒时间戳更新时钟偏差记录 */
fun updateServerDate(serverDateMs: Long) {
if (serverDateMs > 0L) {
lastObservedSkewMs = abs(System.currentTimeMillis() - serverDateMs)
}
}
/**
* 判定电视系统时钟是否明显异常:
* 优先对比最近一次 HTTPS/HTTP 响应 Date 头与本地时间的偏差(不随年份推移腐烂);
* 若尚未发起过网络请求,则用最低合理年份兜底(如电视断电重置回 1970/2015 年)。
*/
fun isSystemClockAbnormal(): Boolean {
val skew = lastObservedSkewMs
if (skew != null) {
return skew > MAX_CLOCK_SKEW_MS
}
return Calendar.getInstance().get(Calendar.YEAR) < MIN_VALID_YEAR
}
/**
* 返回链路状态文案:已连接时区分「系统已验证互联网」与「未验证」。
*/
fun getStatusMessage(context: Context): String {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
?: return context.getString(R.string.network_disconnected)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = cm.activeNetwork ?: return context.getString(R.string.network_disconnected)
val caps = cm.getNetworkCapabilities(network) ?: return context.getString(R.string.network_disconnected)
val typeName = when {
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ->
context.getString(R.string.network_type_wifi)
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ->
context.getString(R.string.network_type_ethernet)
else -> context.getString(R.string.network_type_unknown)
}
val validated = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
if (validated) {
context.getString(R.string.network_connected, typeName)
} else {
context.getString(R.string.network_connected_not_validated, typeName)
}
} else {
@Suppress("DEPRECATION")
val info = cm.activeNetworkInfo
if (info?.isConnected == true) {
val typeName = when (info.type) {
ConnectivityManager.TYPE_WIFI -> context.getString(R.string.network_type_wifi)
ConnectivityManager.TYPE_ETHERNET -> context.getString(R.string.network_type_ethernet)
else -> context.getString(R.string.network_type_unknown)
}
context.getString(R.string.network_connected, typeName)
} else {
context.getString(R.string.network_disconnected)
}
}
}
fun isConnected(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
?: return false
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(network) ?: return false
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
} else {
@Suppress("DEPRECATION")
cm.activeNetworkInfo?.isConnected == true
}
}
/**
* 实际尝试访问互联网(需在后台线程调用),并顺带利用响应 Date 头校准系统时钟异常状态。
* @return true 表示能连上目标 URL,false 表示超时或失败。
*/
fun isInternetReachable(): Boolean {
return try {
val url = URL(INTERNET_CHECK_URL)
val conn = url.openConnection() as? HttpURLConnection ?: return false
PlayerSsl.applyTo(conn, lenient = true)
conn.requestMethod = "HEAD"
conn.connectTimeout = INTERNET_CHECK_TIMEOUT_MS
conn.readTimeout = INTERNET_CHECK_TIMEOUT_MS
conn.instanceFollowRedirects = true
val code = conn.responseCode
updateServerDate(conn.date)
conn.disconnect()
code in 200..399
} catch (_: Exception) {
false
}
}
/**
* 获取局域网 IP 地址(优先 Wi-Fi / 有线)
*/
fun getLocalIpAddress(): String? {
try {
val interfaces = NetworkInterface.getNetworkInterfaces()?.toList() ?: return null
val sorted = interfaces.sortedWith { a, b ->
val aScore = if (a.name.startsWith("wlan") || a.name.startsWith("eth")) 0 else 1
val bScore = if (b.name.startsWith("wlan") || b.name.startsWith("eth")) 0 else 1
aScore.compareTo(bScore)
}
for (ni in sorted) {
if (ni.isLoopback || !ni.isUp) continue
for (addr in ni.inetAddresses) {
if (addr is Inet4Address && !addr.isLoopbackAddress && !addr.hostAddress.orEmpty().startsWith("127.")) {
return addr.hostAddress
}
}
}
} catch (_: Exception) { }
return null
}
}