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)` 括号)
|
- App 内显示统一用纯版本号(不加 `(versionCode)` 括号)
|
||||||
- ⚠️ **发布构建已开启 NewApi 闸门**(`lint { checkReleaseBuilds true; fatal += ['NewApi'] }`):
|
- ⚠️ **发布构建已开启 NewApi 闸门**(`lint { checkReleaseBuilds true; fatal += ['NewApi'] }`):
|
||||||
任何「调用了高于 `minSdk 21` 且未做版本守卫的 API」都会让 `assembleRelease` 直接失败。
|
任何「调用了高于 `minSdk 21` 且未做版本守卫的 API」都会让 `assembleRelease` 直接失败。
|
||||||
|
|||||||
+2
-2
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.local.ktv"
|
applicationId "com.local.ktv"
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 25
|
targetSdk 25
|
||||||
versionCode 24
|
versionCode 25
|
||||||
versionName "1.4.4"
|
versionName "1.4.5"
|
||||||
ndk {
|
ndk {
|
||||||
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
|
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
|
||||||
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
|
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 麦动 KTV 的存储命名空间。
|
* 麦动 KTV 的存储命名空间。
|
||||||
@@ -121,4 +122,48 @@ object AppPaths {
|
|||||||
?: "/storage/emulated/0"
|
?: "/storage/emulated/0"
|
||||||
runCatching { File("$env/muse").deleteRecursively() }
|
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 }
|
.any { "enospc" in it || "no space left" in it }
|
||||||
|
|
||||||
private fun readableError(error: Throwable): Throwable {
|
private fun readableError(error: Throwable): Throwable {
|
||||||
if (isNoSpaceError(error)) {
|
|
||||||
val target = MuseDatabase.defaultDbFile()
|
val target = MuseDatabase.defaultDbFile()
|
||||||
val free = target.parentFile?.usableSpace ?: 0L
|
val free = target.parentFile?.usableSpace ?: 0L
|
||||||
MiliLog.warn(
|
// 完整异常链:老设备上「表面原因」常常误导(例如 ENOSPC 其实不是块空间不足)。
|
||||||
"DB-SYNC 空间不足: free=${formatSize(free)} target=${target.absolutePath} " +
|
val chain = generateSequence(error) { it.cause }
|
||||||
"err=${error.message}",
|
.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)) {
|
||||||
|
// ⚠️ 不能只看异常消息就断言"空间不足":inode 耗尽 / 配额 / ROM 限制同样报 ENOSPC,
|
||||||
|
// 而 usableSpace 可能还显示好几 GB(本机实测:报空间不足时可用 6.04GB)。
|
||||||
|
// 用真实写入探针区分"真的写不进去" vs "其实能写"。
|
||||||
|
val probe = runCatching { AppPaths.probeWrite(target.parentFile!!) }.getOrDefault("?")
|
||||||
return IllegalStateException(
|
return IllegalStateException(
|
||||||
"曲库分区空间不足(当前可用 ${formatSize(free)})," +
|
"曲库写入被系统拒绝(探针 $probe,可用 ${formatSize(free)})\n原始错误:$chain",
|
||||||
"已清理下载临时文件,请释放空间后重试",
|
|
||||||
error,
|
error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val message = error.message?.takeIf { it.isNotBlank() } ?: "曲库下载失败"
|
return IllegalStateException("曲库下载失败:$chain(可用 ${formatSize(free)})", error)
|
||||||
Log.w(TAG, "Database download failed: $message")
|
|
||||||
MiliLog.warn("DB-SYNC download failed: ${error.javaClass.simpleName}: $message")
|
|
||||||
return IllegalStateException(message, error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,11 @@ object MuseDbSync {
|
|||||||
onProgress(100, "曲库就绪")
|
onProgress(100, "曲库就绪")
|
||||||
return target
|
return target
|
||||||
} finally {
|
} 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