feat(android): T-AND-001 工程骨架(AGP 8.7.3 + Gradle 8.9 + Compose)

This commit is contained in:
2026-09-14 20:40:16 +08:00
parent 568664a9aa
commit 2f6212d5bf
21 changed files with 1091 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
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
}
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")
// ---- 权限 ----
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
// ---- 单元测试(T-AND-004 等纯逻辑,跑在 JVM 上,无需设备)----
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlin:kotlin-test:2.0.21")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}
+49
View File
@@ -0,0 +1,49 @@
# OpenDance Studio —— R8 / ProGuard 规则
#
# release 构建开启 minifyEnabled + shrinkResources。以下 keep 规则覆盖:
# - MediaPipe(大量 JNI 与反射,且 tasks-vision 的 AutoValue 生成类需保留)
# - OkHttp / Okio(平台相关实现按类名查找)
# - Kotlin 协程与元数据
# 删掉这些规则会导致 release 包在真机上运行时崩溃(debug 包不会暴露该问题)。
# ---------------------------------------------------------------- MediaPipe
-keep class com.google.mediapipe.** { *; }
-keep interface com.google.mediapipe.** { *; }
-dontwarn com.google.mediapipe.**
# MediaPipe 使用 AutoValue 生成不可变类,依赖注解与泛型签名
-keep class com.google.auto.value.** { *; }
-dontwarn com.google.auto.value.**
-keepclassmembers class * {
@com.google.auto.value.AutoValue.Builder <methods>;
}
# MediaPipe / TFLite 的 native 入口
-keepclasseswithmembernames class * {
native <methods>;
}
# protobuf(MediaPipe 依赖)
-keep class com.google.protobuf.** { *; }
-dontwarn com.google.protobuf.**
# ---------------------------------------------------------------- OkHttp / Okio
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**
-keep class okhttp3.internal.platform.** { *; }
-keep class org.conscrypt.** { *; }
-keep class org.bouncycastle.** { *; }
-keep class org.openjsse.** { *; }
# ---------------------------------------------------------------- Kotlin / 协程
-keepattributes Signature, InnerClasses, EnclosingMethod, *Annotation*
-keep class kotlin.Metadata { *; }
-dontwarn kotlin.**
-keepclassmembers class kotlinx.coroutines.** { volatile <fields>; }
# ---------------------------------------------------------------- AndroidX / Compose
-dontwarn androidx.**
-keep class androidx.camera.** { *; }
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- T-AND-002:相机采集 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- T-AND-006:主机发现(局域网扫描) -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-feature
android:name="android.hardware.camera.any"
android:required="true" />
<application
android:name=".OpenDanceApp"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.OpenDance"
android:usesCleartextTraffic="true"
tools:targetApi="34">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait"
android:configChanges="orientation|screenSize|keyboardHidden"
android:theme="@style/Theme.OpenDance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Binary file not shown.
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 简笔骨架图标:与 OpenDance 的 33 点骨骼主题呼应 -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:translateX="18" android:translateY="18">
<path
android:strokeColor="#4EA1FF"
android:strokeWidth="3"
android:strokeLineCap="round"
android:pathData="M36,8 L36,20 M36,20 L22,34 M36,20 L50,34 M36,20 L36,46 M36,46 L26,66 M36,46 L46,66 M22,34 L16,50 M50,34 L56,50" />
<path
android:fillColor="#FFD166"
android:pathData="M36,4 m-4,0 a4,4 0,1 0,8 0 a4,4 0,1 0,-8 0" />
<path
android:fillColor="#FFD166"
android:pathData="M22,32 m-3,0 a3,3 0,1 0,6 0 a3,3 0,1 0,-6 0" />
<path
android:fillColor="#FFD166"
android:pathData="M50,32 m-3,0 a3,3 0,1 0,6 0 a3,3 0,1 0,-6 0" />
<path
android:fillColor="#FFD166"
android:pathData="M16,50 m-3,0 a3,3 0,1 0,6 0 a3,3 0,1 0,-6 0" />
<path
android:fillColor="#FFD166"
android:pathData="M56,50 m-3,0 a3,3 0,1 0,6 0 a3,3 0,1 0,-6 0" />
<path
android:fillColor="#FFD166"
android:pathData="M36,46 m-3,0 a3,3 0,1 0,6 0 a3,3 0,1 0,-6 0" />
<path
android:fillColor="#FFD166"
android:pathData="M26,66 m-3,0 a3,3 0,1 0,6 0 a3,3 0,1 0,-6 0" />
<path
android:fillColor="#FFD166"
android:pathData="M46,66 m-3,0 a3,3 0,1 0,6 0 a3,3 0,1 0,-6 0" />
</group>
</vector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1B1F2A</color>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">OpenDance 辅助</string>
</resources>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.OpenDance" parent="android:Theme.Material.NoActionBar">
<item name="android:statusBarColor">#1B1F2A</item>
<item name="android:navigationBarColor">#1B1F2A</item>
<item name="android:windowBackground">#1B1F2A</item>
</style>
</resources>