From 1ceba8ef7f0e80a9d46922a9c84eab309c3a4bc7 Mon Sep 17 00:00:00 2001 From: zijunchiang Date: Sat, 26 Sep 2026 16:42:59 +0800 Subject: [PATCH] =?UTF-8?q?V1.4.5:=20=E4=BF=AE=E6=AD=A3=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=94=99=E8=AF=AF=E5=BD=92=E5=9B=A0?= =?UTF-8?q?=20+=20=E6=B8=85=E7=90=86=E6=AE=8B=E7=95=99=E4=B8=B4=E6=97=B6?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=88=E4=B8=BA=E5=AE=9A=E4=BD=8D"?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E4=B8=8D=E8=B6=B3"=E8=AF=AF=E6=8A=A5?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象: 极米 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);签名证书不变。 --- AGENTS.md | 2 +- app/build.gradle | 4 +- app/src/main/java/com/local/ktv/AppPaths.kt | 45 +++++++++++++++++++ .../com/local/ktv/DatabaseBootstrapper.kt | 28 +++++++----- app/src/main/java/com/local/ktv/MuseDbSync.kt | 6 ++- 5 files changed, 69 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0ebab2a..1c5e8af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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` 直接失败。 diff --git a/app/build.gradle b/app/build.gradle index 155a160..bd90d08 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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,否则 diff --git a/app/src/main/java/com/local/ktv/AppPaths.kt b/app/src/main/java/com/local/ktv/AppPaths.kt index eda383d..33da229 100644 --- a/app/src/main/java/com/local/ktv/AppPaths.kt +++ b/app/src/main/java/com/local/ktv/AppPaths.kt @@ -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()}]" + } } diff --git a/app/src/main/java/com/local/ktv/DatabaseBootstrapper.kt b/app/src/main/java/com/local/ktv/DatabaseBootstrapper.kt index f32fd0f..c36f06b 100644 --- a/app/src/main/java/com/local/ktv/DatabaseBootstrapper.kt +++ b/app/src/main/java/com/local/ktv/DatabaseBootstrapper.kt @@ -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) } } diff --git a/app/src/main/java/com/local/ktv/MuseDbSync.kt b/app/src/main/java/com/local/ktv/MuseDbSync.kt index 5849dc2..f04a40f 100644 --- a/app/src/main/java/com/local/ktv/MuseDbSync.kt +++ b/app/src/main/java/com/local/ktv/MuseDbSync.kt @@ -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() } } }