V1.4.0: 修复断网重连后无法下载 + 网络状态检测维护
根因(断网重连后再也下载不了、日志却显示"歌曲源获取成功"):
SongApiClient.clearTokenCache() 一直是**空实现**。JS 侧 window.api.tokens 与
_apiInitPromise 因此被一直复用; 而歌曲源按「出口 IP + device」识别、试用窗口
也记在出口 IP 上(见 Eac19WindowGuard), 断网/重连(尤其换出口 IP)后旧 token 已失效 ——
取址这一步看起来成功, 真正下载却 403/失败。
1) 网络状态检测(新增 KtvNetworkMonitor)
走 ConnectivityManager 回调(不轮询)维护全局 online, 状态变化才回调主线程;
全局可读 online 供下载层判断失败是否由断网引起。
2) 播放页右上角「网络未连接」提示
全屏播放页控制层 header 内、标题右侧新增红色徽标(不遮挡右上角二维码),
断网显示、恢复自动隐藏。
3) 网络恢复/换网后的统一维护 refreshNetworkState()
- 重建歌曲源会话: clearTokenCache() 真正下发 KtvJsBridge.resetApiSession()
(置空 window.api 与 _apiInitPromise, 下次取址重新构造 API 实例、回到默认源顺序
并重新登录拿新 token);
- 复位 Eac19WindowGuard 窗口守卫; 重解析网卡 MAC + 刷新日志上下文;
- 重启本地点歌服务并刷新二维码;
- 自动重试断网期间失败的下载(最多 8 首)。
断网恢复走该路径(带提示), 换网换 IP 也走同一路径(静默)。
4) 下载层网络失败跟踪(SongOkDownloadManager)
结合 KtvNetworkMonitor.online 与异常类型(UnknownHost/Connect/SocketTimeout/
NoRouteToHost/SSL/EOF)及 HTTP 0 判定网络类失败, 记入 networkFailed;
成功后清除、手动取消不入集合; retryNetworkFailed() 供恢复时调用。
5) 顺带修掉一个隐患: 离线时 localIp() 为空会让 remoteNetworkMonitor 每 5s 误判
"IP 变化"而反复重启本地点歌服务; 现已加空值短路, 离线只等网络回调。
版本号 1.3.9 -> 1.4.0(versionCode 15 -> 16)。按约定本次不发布 Release, 仅本地打包测试。
This commit is contained in:
@@ -8,6 +8,8 @@ import com.liulishuo.okdownload.core.cause.EndCause
|
||||
import com.liulishuo.okdownload.core.cause.ResumeFailedCause
|
||||
import com.liulishuo.okdownload.core.listener.DownloadListener2
|
||||
import java.io.File
|
||||
import java.util.Collections
|
||||
import java.util.LinkedHashMap
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -16,6 +18,8 @@ import java.util.concurrent.atomic.AtomicLong
|
||||
object SongOkDownloadManager {
|
||||
private const val TAG = "SongDownload"
|
||||
const val MIN_VALID_FILE_SIZE = SongFileValidator.MIN_VALID_FILE_SIZE
|
||||
/** 网络恢复后最多自动重试几首(防止一次拉起过多并发下载)。 */
|
||||
private const val MAX_NETWORK_RETRY = 8
|
||||
private const val MAX_RETRY_COUNT = 3
|
||||
private const val STALL_TIMEOUT_MS = 25_000L
|
||||
|
||||
@@ -96,6 +100,8 @@ object SongOkDownloadManager {
|
||||
}
|
||||
if (url.isEmpty()) {
|
||||
pending.remove(key)
|
||||
// 取不到地址通常也是网络/源站不可达, 记下来供网络恢复后自动重试。
|
||||
if (isNetworkFailure(null, 0)) networkFailed[key] = song
|
||||
callback?.onDownloadFailed(song, "无法生成下载地址")
|
||||
} else {
|
||||
startDownloadTask(song, url, callback, 0)
|
||||
@@ -113,6 +119,7 @@ object SongOkDownloadManager {
|
||||
pending.remove(key)
|
||||
tasks.remove(key)?.let { task ->
|
||||
manuallyCanceled.add(key)
|
||||
networkFailed.remove(key)
|
||||
task.cancel()
|
||||
clearBrokenDownload(task, getLocalFile(song))
|
||||
}
|
||||
@@ -124,6 +131,43 @@ object SongOkDownloadManager {
|
||||
@JvmStatic
|
||||
fun getDownloadProgress(song: Song): Int = progress[songKey(song)] ?: 0
|
||||
|
||||
/**
|
||||
* 网络恢复后自动重试「因网络原因失败」的下载(最多 [MAX_NETWORK_RETRY] 首)。
|
||||
*
|
||||
* 断网期间的失败大多是连不上/超时/HTTP 0,属于可恢复;恢复后重试一次,避免用户
|
||||
* 手动重新点歌。手动取消的歌不会进入这个集合。
|
||||
*
|
||||
* @return 实际发起重试的数量
|
||||
*/
|
||||
@JvmStatic
|
||||
fun retryNetworkFailed(): Int {
|
||||
val retry = synchronized(networkFailed) {
|
||||
networkFailed.values.toList().also { networkFailed.clear() }
|
||||
}
|
||||
if (retry.isEmpty()) return 0
|
||||
val limited = retry.take(MAX_NETWORK_RETRY)
|
||||
Log.i(TAG, "network recovered, retrying ${limited.size} failed download(s)")
|
||||
io.execute {
|
||||
limited.forEach { song -> runCatching { download(song, callbacks[songKey(song)]) } }
|
||||
}
|
||||
return limited.size
|
||||
}
|
||||
|
||||
/** 判断一个失败是否属于「网络原因」(断网/超时/连不上/HTTP 0)。 */
|
||||
private fun isNetworkFailure(realCause: Exception?, httpCode: Int): Boolean {
|
||||
if (!KtvNetworkMonitor.online) return true
|
||||
if (httpCode == 0) return true
|
||||
val cause = realCause ?: return false
|
||||
return cause is java.net.UnknownHostException ||
|
||||
cause is java.net.ConnectException ||
|
||||
cause is java.net.SocketTimeoutException ||
|
||||
cause is java.net.NoRouteToHostException ||
|
||||
cause is java.net.SocketException ||
|
||||
cause is javax.net.ssl.SSLException ||
|
||||
cause is java.io.EOFException ||
|
||||
cause is java.io.InterruptedIOException
|
||||
}
|
||||
|
||||
private fun buildDownloadUrl(song: Song, forceRefresh: Boolean = false): String {
|
||||
if (!forceRefresh) {
|
||||
song.downloadUrl?.takeIf(String::isNotBlank)?.let { cached ->
|
||||
@@ -222,6 +266,10 @@ object SongOkDownloadManager {
|
||||
} else {
|
||||
pending.remove(key)
|
||||
progress.remove(key)
|
||||
if (isNetworkFailure(realCause, httpResponseCode)) {
|
||||
networkFailed[key] = song
|
||||
Log.w(TAG, "download failed by network: ${song.title} (${cause.name})")
|
||||
}
|
||||
cb?.onDownloadFailed(song, cause.name + (realCause?.message?.let { ": $it" } ?: ""))
|
||||
}
|
||||
return
|
||||
@@ -292,6 +340,7 @@ object SongOkDownloadManager {
|
||||
}
|
||||
pending.remove(key)
|
||||
progress.remove(key)
|
||||
networkFailed.remove(key)
|
||||
song.path = target.absolutePath
|
||||
callback?.onDownloadProgress(song, 100)
|
||||
callback?.onDownloadComplete(song, target.absolutePath)
|
||||
@@ -371,6 +420,9 @@ object SongOkDownloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** 因网络原因失败的歌曲(网络恢复后自动重试一次)。 */
|
||||
private val networkFailed = Collections.synchronizedMap(LinkedHashMap<String, Song>())
|
||||
|
||||
private fun songKey(song: Song): String = song.id?.takeIf(String::isNotEmpty)
|
||||
?: song.filename?.takeIf(String::isNotEmpty)
|
||||
?: song.title.orEmpty()
|
||||
|
||||
Reference in New Issue
Block a user