V1.4.5: 修正下载失败的错误归因 + 清理残留临时文件(为定位"空间不足"误报)
现象: 极米 H1S 上报告「曲库分区空间不足(当前可用 6.04 GB)」——数字自相矛盾。 核实(本地实测 Muse 协议): - 曲库压缩包 430,575,974 字节;从 gzip 尾部 ISIZE 读出解压后为 965MB, 峰值占用约 1.4GB,6.04GB 可用空间根本不构成空间不足。 - 结论: 真正抛出的异常另有原因(消息里含 no space left), 被 isNoSpaceError() 只按消息文本匹配而误判成了"空间不足"。 修复: - readableError 不再只看异常消息: 报告完整异常链 + 可用空间, 并用真实写入探针(AppPaths.probeWrite,写 8MB 再删)区分 "真的写不进去" 与 "其实能写"(inode 耗尽/配额/ROM 限制同样报 ENOSPC, 而 usableSpace 仍显示数 GB)。 - 失败时上报单行完整诊断到远程日志: DB-SYNC download failed | <异常链> | root/free/total/envState/probe/proc-mounts。 - 界面失败文案直接显示原始异常,不再用可能误导的"空间不足"结论。 - AppPaths 新增 probeWrite() 与 diagnose()(含 /proc/mounts 相关挂载点)。 - MuseDbSync 失败时同时清理 muse.db.gz.download 与 muse.db.download: 原先失败只删前者,残留的 965MB 分片在反复重试时会白占空间。 版本 1.4.4(24) 到 1.4.5(25);签名证书不变。
This commit is contained in:
@@ -160,7 +160,7 @@ $BT/apksigner verify 目标名.apk
|
||||
|
||||
### 版本号
|
||||
|
||||
- `app/build.gradle`:`versionName "1.4.4"` / `versionCode 24`(18~21 被本机临时诊断包占用过)
|
||||
- `app/build.gradle`:`versionName "1.4.5"` / `versionCode 25`(18~21 被本机临时诊断包占用过)
|
||||
- App 内显示统一用纯版本号(不加 `(versionCode)` 括号)
|
||||
- ⚠️ **发布构建已开启 NewApi 闸门**(`lint { checkReleaseBuilds true; fatal += ['NewApi'] }`):
|
||||
任何「调用了高于 `minSdk 21` 且未做版本守卫的 API」都会让 `assembleRelease` 直接失败。
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.local.ktv"
|
||||
minSdk 21
|
||||
targetSdk 25
|
||||
versionCode 24
|
||||
versionName "1.4.4"
|
||||
versionCode 25
|
||||
versionName "1.4.5"
|
||||
ndk {
|
||||
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
|
||||
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
/**
|
||||
* 麦动 KTV 的存储命名空间。
|
||||
@@ -121,4 +122,48 @@ object AppPaths {
|
||||
?: "/storage/emulated/0"
|
||||
runCatching { File("$env/muse").deleteRecursively() }
|
||||
}
|
||||
|
||||
private fun size(text: Long): String =
|
||||
String.format(java.util.Locale.ROOT, "%.2fGB", text / 1024.0 / 1024.0 / 1024.0)
|
||||
|
||||
/**
|
||||
* 真实写入探针:写 [sizeMB] 兆再删掉,验证「**现在真的能写**」。
|
||||
*
|
||||
* `usableSpace` 只反映**块空间**;inode 耗尽、配额、FUSE/ROM 限制都会返回 ENOSPC
|
||||
* 但 still 报着几 GB 可用 —— 所以判断"是不是空间问题"必须靠真实写入,而不是看数字。
|
||||
*/
|
||||
fun probeWrite(dir: File, sizeMB: Int = 8): String = runCatching {
|
||||
val probe = File(dir, ".write_probe.tmp")
|
||||
try {
|
||||
val buffer = ByteArray(1024 * 1024)
|
||||
FileOutputStream(probe).use { out ->
|
||||
repeat(sizeMB) { out.write(buffer) }
|
||||
out.flush()
|
||||
out.fd.sync()
|
||||
}
|
||||
"ok(${sizeMB}MB)"
|
||||
} finally {
|
||||
runCatching { probe.delete() }
|
||||
}
|
||||
}.getOrElse { "${it.javaClass.simpleName}: ${it.message}" }
|
||||
|
||||
private fun mountLines(): String = runCatching {
|
||||
File("/proc/mounts").readLines()
|
||||
.filter { line ->
|
||||
line.contains("sdcard") || line.contains("/data ") ||
|
||||
line.contains("/storage") || line.contains("/mnt")
|
||||
}
|
||||
.take(12)
|
||||
.joinToString(" | ") { it.substringBefore(" ").trim() }
|
||||
}.getOrNull() ?: "(unreadable)"
|
||||
|
||||
/** 一次性诊断串:空间、真实可写性、挂载点。失败时上报远程日志并显示在界面上。 */
|
||||
fun diagnose(): String {
|
||||
val dir = databaseDir
|
||||
val free = runCatching { dir.usableSpace }.getOrDefault(0L)
|
||||
val total = runCatching { dir.totalSpace }.getOrDefault(0L)
|
||||
val envState = runCatching { Environment.getExternalStorageState() }.getOrNull()
|
||||
return "root=${root.absolutePath} free=${size(free)} total=${size(total)} " +
|
||||
"envState=$envState probe=${probeWrite(dir)} mounts=[${mountLines()}]"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,22 +88,26 @@ object DatabaseBootstrapper {
|
||||
.any { "enospc" in it || "no space left" in it }
|
||||
|
||||
private fun readableError(error: Throwable): Throwable {
|
||||
val target = MuseDatabase.defaultDbFile()
|
||||
val free = target.parentFile?.usableSpace ?: 0L
|
||||
// 完整异常链:老设备上「表面原因」常常误导(例如 ENOSPC 其实不是块空间不足)。
|
||||
val chain = generateSequence(error) { it.cause }
|
||||
.take(5)
|
||||
.joinToString(" <- ") { "${it.javaClass.simpleName}: ${it.message}" }
|
||||
val diagnose = runCatching { AppPaths.diagnose() }.getOrDefault("(diagnose failed)")
|
||||
MiliLog.warn("DB-SYNC download failed | $chain | $diagnose")
|
||||
Log.w(TAG, "Database download failed: $chain | $diagnose")
|
||||
|
||||
if (isNoSpaceError(error)) {
|
||||
val target = MuseDatabase.defaultDbFile()
|
||||
val free = target.parentFile?.usableSpace ?: 0L
|
||||
MiliLog.warn(
|
||||
"DB-SYNC 空间不足: free=${formatSize(free)} target=${target.absolutePath} " +
|
||||
"err=${error.message}",
|
||||
)
|
||||
// ⚠️ 不能只看异常消息就断言"空间不足":inode 耗尽 / 配额 / ROM 限制同样报 ENOSPC,
|
||||
// 而 usableSpace 可能还显示好几 GB(本机实测:报空间不足时可用 6.04GB)。
|
||||
// 用真实写入探针区分"真的写不进去" vs "其实能写"。
|
||||
val probe = runCatching { AppPaths.probeWrite(target.parentFile!!) }.getOrDefault("?")
|
||||
return IllegalStateException(
|
||||
"曲库分区空间不足(当前可用 ${formatSize(free)})," +
|
||||
"已清理下载临时文件,请释放空间后重试",
|
||||
"曲库写入被系统拒绝(探针 $probe,可用 ${formatSize(free)})\n原始错误:$chain",
|
||||
error,
|
||||
)
|
||||
}
|
||||
val message = error.message?.takeIf { it.isNotBlank() } ?: "曲库下载失败"
|
||||
Log.w(TAG, "Database download failed: $message")
|
||||
MiliLog.warn("DB-SYNC download failed: ${error.javaClass.simpleName}: $message")
|
||||
return IllegalStateException(message, error)
|
||||
return IllegalStateException("曲库下载失败:$chain(可用 ${formatSize(free)})", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,11 @@ object MuseDbSync {
|
||||
onProgress(100, "曲库就绪")
|
||||
return target
|
||||
} finally {
|
||||
gzFile.delete()
|
||||
// 失败时两个临时文件都要清掉:残留的 .gz.download(430MB) + .download(965MB)
|
||||
// 白占约 1.4GB,反复重试时正是"空间不足"的常见来源。
|
||||
// 成功路径上 tmpDb 已 rename 成正式库,这里的 delete 是空操作。
|
||||
runCatching { gzFile.delete() }
|
||||
runCatching { tmpDb.delete() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user