V1.4.1: 修复 Android 5.1.1 设备一打开就闪退(API 守卫缺失)

根因(极米 H1S / MStar 6A928 / GMUI2.0 / Android 5.1.1):
- MainActivity 把 API 23 才有的 android.media.AudioDeviceCallback 写成了
  **字段初始化器**(private val micDeviceCallback = object : ...)。
  字段初始化在 Activity 构造期执行,早于 onCreate 里的 SDK_INT 判断,
  于是下面的 registerMicInputWatcher() 版本守卫形同虚设 ——
  构造 MainActivity 即抛 NoClassDefFoundError,表现为一打开就闪退。
- 顺带修复 36 处同类未守卫的高版本 API 调用(lint NewApi 全量清零):
  * AudioDeviceCallback / AudioDeviceInfo.isSource/getType (API 23)
    → 改为可空字段 + 仅在 SDK_INT>=23 分支按需创建(@TargetApi)
  * HashMap.putIfAbsent ×5 (API 24) → MutableMap.getOrPut
  * Comparator.comparing ×3 + comparingInt ×1 (API 24) → Kotlin compareBy
    (同时移除 java.util.function 导入)
  * ConcurrentHashMap.newKeySet ×2 (API 24) → Collections.newSetFromMap
    (连带 16 处 KeySetView.add/remove/contains)
  * ConcurrentHashMap.computeIfAbsent (API 24) → ConcurrentHashMap.putIfAbsent(API 1)
  * URLConnection.getContentLengthLong ×5 (API 24) → 新增 Compat.contentLengthCompat()
  * ByteArrayOutputStream.toString(Charset) (API 33) → String(bytes, UTF_8)

防回归:
- app/build.gradle 打开 lint 闸门: checkReleaseBuilds true + abortOnError true +
  fatal += NewApi(并 disable ExpiredTargetSdkVersion,避免它挤占闸门)。
  已实测: 植入一个 NewApi 违规后 assembleRelease 直接 FAILED。
- AGENTS.md 新增「低版本设备闪退(API 守卫)」条目,记录根因、替代写法与自查命令。

版本号 1.4.0(16) → 1.4.1(17);签名证书不变,可覆盖安装。
This commit is contained in:
zijunchiang
2026-09-26 13:59:51 +08:00
parent 96c83d27b0
commit 40d1fec719
9 changed files with 116 additions and 46 deletions
+26
View File
@@ -0,0 +1,26 @@
package com.local.ktv
import android.os.Build
import java.net.URLConnection
/**
* 低版本兼容工具。
*
* 背景:`minSdk 21`,但代码里用过若干 API 22 不存在的方法,在 Android 5.x
* (如极米 H1S / GMUI2.0 / Android 5.1.1)上会在运行期抛 `NoSuchMethodError`。
* 这里把这类调用收敛成带版本判断的兼容函数。
*/
/**
* 读取 HTTP 响应体长度。
*
* `URLConnection.getContentLengthLong()` 是 **API 24(N)** 才加入的;API 21~23 上直接调用
* 会抛 `NoSuchMethodError`。低版本回退到 `getContentLength()`(int,2GB 以内等价)。
*/
fun URLConnection.contentLengthCompat(): Long =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
contentLengthLong
} else {
@Suppress("DEPRECATION")
contentLength.toLong()
}