Files
opendance-android/app/build.gradle.kts
T
zijunchiang e9ea1b5811 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 项单元测试
2026-09-14 22:43:03 +08:00

193 lines
7.3 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import java.util.Properties
// OpenDance Studio —— Android 辅助 App(应用模块)
//
// 契约:contracts/websocket-keypoints.md v0.1.0(本端为**发送方**)
// contracts/skeleton-topology.md v0.1.0(MediaPipe 33 点)
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
namespace = "com.opendance.studio"
compileSdk = 34
defaultConfig {
applicationId = "com.opendance.studio"
minSdk = 29 // Android 10(文档 T-AND-009 验收要求 Android 10+)
targetSdk = 34
versionCode = 1
versionName = "0.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// 契约版本,供关于页与日志显示
buildConfigField("String", "CONTRACT_VERSION", "\"0.1.0\"")
buildConfigField("String", "WS_SUBPROTOCOL", "\"opendance.pose.v1\"")
}
/**
* 签名配置:优先读取 keystore.properties(已被 .gitignore 忽略),
* 否则读取环境变量;两者都没有时回退到 debug 签名。
*
* 这样 `./gradlew assembleRelease` 在没有任何私密文件的情况下也能产出**已签名**的
* APK(用 debug key),便于 CI 与验收;正式发布必须提供自己的 keystore
* (见 build/gen-keystore.sh 与 README.txt)。
*/
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().use { keystoreProperties.load(it) }
}
fun signingValue(key: String, env: String): String? =
keystoreProperties.getProperty(key) ?: System.getenv(env)
val releaseStoreFile = signingValue("storeFile", "KEYSTORE_PATH")
val hasReleaseKeystore = releaseStoreFile != null && file(releaseStoreFile).exists()
signingConfigs {
if (hasReleaseKeystore) {
create("release") {
storeFile = file(releaseStoreFile!!)
storePassword = signingValue("storePassword", "KEYSTORE_PASSWORD")
keyAlias = signingValue("keyAlias", "KEY_ALIAS")
keyPassword = signingValue("keyPassword", "KEY_PASSWORD")
}
}
}
buildTypes {
debug {
applicationIdSuffix = ".debug"
isMinifyEnabled = false
// debug 保留全部 ABI,方便在模拟器(x86_64)上调试
ndk {
abiFilters.clear()
abiFilters.addAll(listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64"))
}
}
release {
// 发布包只打包真实手机用得到的 ABI。
// x86/x86_64 仅供模拟器使用,却占掉约 27 MB(几乎是发布包的一半),
// 对真机用户纯属浪费。
ndk {
abiFilters.clear()
abiFilters.addAll(listOf("arm64-v8a", "armeabi-v7a"))
}
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
signingConfig = if (hasReleaseKeystore) {
signingConfigs.getByName("release")
} else {
// 回退:用 debug 签名,保证 CI 上 assembleRelease 也能产出已签名 APK
signingConfigs.getByName("debug")
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
buildConfig = true
}
packaging {
resources {
excludes += setOf(
"/META-INF/{AL2.0,LGPL2.1}",
"/META-INF/DEPENDENCIES",
"/META-INF/LICENSE*",
"/META-INF/NOTICE*",
)
}
// MediaPipe 的 .task 模型必须保持不压缩,否则运行时按文件路径读取会失败
androidResources {
noCompress += "task"
}
}
testOptions {
unitTests {
isReturnDefaultValues = true
// Robolectric 需要真实资源与 Manifest 才能组合 Compose 界面
isIncludeAndroidResources = true
}
}
lint {
abortOnError = false
}
}
dependencies {
// ---- Android 基础 ----
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")
implementation("androidx.activity:activity-compose:1.9.2")
// ---- Compose ----
val composeBom = platform("androidx.compose:compose-bom:2024.10.01")
implementation(composeBom)
androidTestImplementation(composeBom)
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
debugImplementation("androidx.compose.ui:ui-tooling")
// ---- CameraX(T-AND-002)----
val cameraX = "1.4.0"
implementation("androidx.camera:camera-core:$cameraX")
implementation("androidx.camera:camera-camera2:$cameraX")
implementation("androidx.camera:camera-lifecycle:$cameraX")
implementation("androidx.camera:camera-view:$cameraX")
// ---- MediaPipe Pose 端侧推理(T-AND-003)----
implementation("com.google.mediapipe:tasks-vision:0.10.35")
// ---- WebSocket 推流(T-AND-005)----
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// 权限:使用平台 Activity Result API(androidx.activity)。
// 原先用 accompanist-permissions 0.34.0,它发布于 2024-02,会传递引入
// compose animation-core 1.6.0,与 Compose BOM 存在版本偏斜风险;
// 该库已进入维护状态,官方也建议改用 Activity Result API。
// ---- 单元测试(T-AND-004 等纯逻辑,跑在 JVM 上,无需设备)----
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlin:kotlin-test:2.0.21")
// Robolectric:在 JVM 上跑 Android 框架 + Compose,用于验证
// 「进入运行页即崩溃」这类只在真机/运行时才暴露的缺陷
testImplementation("org.robolectric:robolectric:4.13")
testImplementation("androidx.test:core-ktx:1.6.1")
testImplementation("androidx.test.ext:junit:1.2.1")
testImplementation("androidx.compose.ui:ui-test-junit4")
// 必须用 debugImplementation:它往 debug 变体的合并 Manifest 里注册
// androidx.activity.ComponentActivity,Compose 测试规则要靠它启动宿主 Activity。
// 放 testImplementation 不会参与 Manifest 合并,运行时报
// "Unable to resolve activity for Intent ... ComponentActivity"。
debugImplementation("androidx.compose.ui:ui-test-manifest")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}