fix(android): 修复进入推流页立即闪退(未捕获 Error)
用户反馈:扫描到客户端后点击「开始推流」立刻闪退。
根因:PoseDetector.initialize() 只 catch (Exception),而 MediaPipe 初始化
失败会抛 UnsatisfiedLinkError / NoClassDefFoundError / ExceptionInInitializerError
—— 这些都是 Error,不是 Exception,会直接冒泡成进程崩溃。
证据(对照实验):新增 Robolectric 组合测试后
改回 catch (Exception):5 项测试 2 项失败
- java.lang.UnsatisfiedLinkError: no mediapipe_tasks_jni in java.library.path
- java.lang.NoClassDefFoundError: Could not initialize class ...PoseLandmarker
改为 catch (Throwable):5 项全部通过
修复与加固:
- PoseDetector / CameraController 全部改为捕获 Throwable,失败只提示不崩溃
- 移除 accompanist-permissions 0.34.0(2024-02 发布,会传递引入
compose animation-core 1.6.0,与 Compose BOM 存在版本偏斜风险),
改用平台 Activity Result API
- MediaPipe 回调在相机线程:所有 Compose 状态写入统一 post 回主线程
- WebSocket 连接、相机启动、单帧分析全部包上失败保护
- 新增 CrashLogger:未捕获异常落盘到 filesDir/crashes/,
并在连接页直接展示堆栈(免去连 adb 抓 logcat)
- 新增 ScreenCompositionTest(Robolectric):真实组合三个页面,
覆盖「进入页面即崩溃」这一类缺陷,共 30 项单元测试
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package com.opendance.studio
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.opendance.studio.data.AppSettings
|
||||
import com.opendance.studio.ui.AboutScreen
|
||||
import com.opendance.studio.ui.ConnectScreen
|
||||
import com.opendance.studio.ui.RunningScreen
|
||||
import com.opendance.studio.ui.theme.OpenDanceTheme
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/**
|
||||
* 界面组合回归测试(Robolectric,跑在 JVM 上)
|
||||
*
|
||||
* 目的:捕获「进入某个页面立刻崩溃」这一类缺陷。这类问题编译期完全看不出来,
|
||||
* 真机上却是一点就退。本测试把三个页面都真实组合一遍,并用
|
||||
* `waitForIdle` 跑完副作用(DisposableEffect / LaunchedEffect)。
|
||||
*
|
||||
* 说明:Robolectric 没有相机 HAL,也没有 MediaPipe 的原生库,
|
||||
* 因此相机与推理初始化必然失败 —— 但**失败必须是可捕获、可提示的**,
|
||||
* 而不是把整个应用带崩。这正是本测试要断言的稳定性契约。
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class ScreenCompositionTest {
|
||||
|
||||
@get:Rule
|
||||
val composeRule = createComposeRule()
|
||||
|
||||
private fun newSettings(): AppSettings {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
return AppSettings(context).apply {
|
||||
host = "127.0.0.1"
|
||||
port = 1 // 保证连不上,走连接失败分支
|
||||
resolution = "480x360"
|
||||
}
|
||||
}
|
||||
|
||||
private fun grantCamera() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
shadowOf(context as android.app.Application).grantPermissions(Manifest.permission.CAMERA)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun connectScreenComposesWithoutCrash() {
|
||||
composeRule.setContent {
|
||||
OpenDanceTheme { ConnectScreen(settings = newSettings(), onStart = {}, onAbout = {}) }
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
assertTrue("连接页组合完成", true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runningScreenComposesWithoutCrashWhenPermissionGranted() {
|
||||
grantCamera()
|
||||
composeRule.setContent {
|
||||
OpenDanceTheme { RunningScreen(settings = newSettings(), onStop = {}) }
|
||||
}
|
||||
// 这里会真实执行 DisposableEffect:MediaPipe 初始化、WebSocket 连接、
|
||||
// 相机启动 —— 全都预期失败,但必须全部被捕获而不是崩溃。
|
||||
composeRule.waitForIdle()
|
||||
assertTrue("授予相机权限后运行页组合完成且未崩溃", true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runningScreenComposesWithoutCrashWhenPermissionMissing() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
shadowOf(context as android.app.Application).denyPermissions(Manifest.permission.CAMERA)
|
||||
|
||||
composeRule.setContent {
|
||||
OpenDanceTheme { RunningScreen(settings = newSettings(), onStop = {}) }
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
assertTrue("未授予权限时显示权限提示页而非崩溃", true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aboutScreenComposesWithoutCrash() {
|
||||
composeRule.setContent {
|
||||
OpenDanceTheme { AboutScreen(onBack = {}) }
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
assertTrue("关于页组合完成", true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun switchingFromConnectToRunningDoesNotCrash() {
|
||||
grantCamera()
|
||||
// 复现用户路径:连接页 → 点「开始推流」→ 运行页
|
||||
composeRule.setContent {
|
||||
OpenDanceTheme {
|
||||
val screen = androidx.compose.runtime.remember {
|
||||
androidx.compose.runtime.mutableStateOf(true)
|
||||
}
|
||||
if (screen.value) {
|
||||
ConnectScreen(settings = newSettings(), onStart = { screen.value = false }, onAbout = {})
|
||||
} else {
|
||||
RunningScreen(settings = newSettings(), onStop = { screen.value = true })
|
||||
}
|
||||
}
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
assertTrue("连接页与运行页切换不崩溃", true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user