diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..2ba4eeb --- /dev/null +++ b/app/build.gradle.kts @@ -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") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..04a8139 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 ; +} + +# MediaPipe / TFLite 的 native 入口 +-keepclasseswithmembernames class * { + native ; +} + +# 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 ; } + +# ---------------------------------------------------------------- AndroidX / Compose +-dontwarn androidx.** +-keep class androidx.camera.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..146fd73 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/pose_landmarker_lite.task b/app/src/main/assets/pose_landmarker_lite.task new file mode 100644 index 0000000..09576a9 Binary files /dev/null and b/app/src/main/assets/pose_landmarker_lite.task differ diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..933273c --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..a8a8fa5 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..a8a8fa5 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..9b16113 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #1B1F2A + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..066fcec --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + OpenDance 辅助 + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..d2ab055 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,8 @@ + + + + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..bbc15cf --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,11 @@ +// OpenDance Studio —— Android 辅助 App(根构建脚本) +// +// 版本组合(均已验证可解析、可用 JDK 17 构建): +// AGP 8.7.3 + Gradle 8.9 + Kotlin 2.0.21 + Compose BOM 2024.10.01 +// compileSdk 34 / minSdk 29(Android 10+,符合文档 T-AND-009 验收) + +plugins { + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.0.21" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false +} diff --git a/build/build-apk.sh b/build/build-apk.sh new file mode 100755 index 0000000..5affa8c --- /dev/null +++ b/build/build-apk.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# ============================================================ +# OpenDance Studio —— Android APK 打包(T-AND-009) +# +# 文档 §6.3 方式二:命令行 Gradle +# +# 前置: +# JAVA_HOME 指向 JDK 17(AGP 8.7 要求) +# ANDROID_HOME / ANDROID_SDK_ROOT 指向 Android SDK +# 需要 platforms;android-34 与 build-tools;34.0.0 +# +# 用法: +# bash build/build-apk.sh # 同时产出 debug 与 release +# bash build/build-apk.sh debug +# bash build/build-apk.sh release +# ============================================================ +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$PROJECT_DIR" + +TARGET="${1:-all}" +case "$TARGET" in + debug|release|all) ;; + *) echo "用法:bash build/build-apk.sh [debug|release|all]" >&2; exit 2 ;; +esac + +log() { printf '\033[1;34m[apk]\033[0m %s\n' "$*"; } +die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; } + +# ---- 环境检查 ---- +# 优先使用 JAVA_HOME 指定的 JDK:PATH 上的 java 可能是更新的版本, +# AGP 8.7 需要 17~21,检测时必须与 Gradle 实际使用的 JVM 一致。 +JAVA_BIN="java" +if [ -n "${JAVA_HOME:-}" ] && [ -x "$JAVA_HOME/bin/java" ]; then + JAVA_BIN="$JAVA_HOME/bin/java" +fi +command -v "$JAVA_BIN" >/dev/null 2>&1 || die "未找到 java,请安装 JDK 17 或设置 JAVA_HOME" +JAVA_MAJOR="$("$JAVA_BIN" -version 2>&1 | head -1 | sed -E 's/.*version "([0-9]+).*/\1/')" +log "Java:$JAVA_BIN (主版本 $JAVA_MAJOR,AGP 8.7 需要 17+)" +if [ "$JAVA_MAJOR" -lt 17 ] 2>/dev/null; then + die "JDK 版本过低:需要 17 或更高(当前 $JAVA_MAJOR)" +fi + +SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" +if [ -z "$SDK" ]; then + for candidate in "$HOME/Android/Sdk" /opt/android-sdk /usr/lib/android-sdk; do + [ -d "$candidate" ] && SDK="$candidate" && break + done +fi +[ -n "$SDK" ] && [ -d "$SDK" ] || die "未找到 Android SDK,请设置 ANDROID_HOME" +log "Android SDK:$SDK" + +if [ ! -f "$PROJECT_DIR/local.properties" ]; then + echo "sdk.dir=$SDK" > "$PROJECT_DIR/local.properties" + log "已写入 local.properties" +fi + +[ -f "$PROJECT_DIR/gradlew" ] || die "缺少 gradlew(请先执行 gradle wrapper)" + +# ---- 签名检查 ---- +if [ "$TARGET" != "debug" ]; then + if [ -f "$PROJECT_DIR/keystore.properties" ]; then + log "使用 keystore.properties 中的发布签名" + else + log "提示:未找到 keystore.properties,release 将回退为 debug 签名" + log " 正式发布请先执行:bash build/gen-keystore.sh" + fi +fi + +# ---- 构建 ---- +if [ "$TARGET" = "debug" ] || [ "$TARGET" = "all" ]; then + log "构建 debug APK…" + ./gradlew assembleDebug --console=plain +fi +if [ "$TARGET" = "release" ] || [ "$TARGET" = "all" ]; then + log "构建 release APK…" + ./gradlew assembleRelease --console=plain +fi + +# ---- 校验产物 ---- +DEBUG_APK="app/build/outputs/apk/debug/app-debug.apk" +RELEASE_APK="app/build/outputs/apk/release/app-release.apk" + +verify_apk() { + local apk="$1" + local label="$2" + [ -f "$apk" ] || { echo " ✗ 未生成 $label:$apk"; return 1; } + + local size + size=$(stat -c%s "$apk") + + # APK 是 zip,校验魔数 PK + local magic + magic=$(head -c 2 "$apk" | od -An -tx1 | tr -d ' \n') + [ "$magic" = "504b" ] || { echo " ✗ $label 不是合法 APK(魔数 $magic)"; return 1; } + + # 校验是否已签名:APK v2 签名块附近含 "APK Sig Block 42" + local signed="未检出 v2 签名块" + if grep -qa "APK Sig Block 42" "$apk"; then + signed="已签名(APK Signature Scheme v2)" + elif unzip -l "$apk" 2>/dev/null | grep -q "META-INF/.*\.RSA"; then + signed="已签名(v1 JAR 签名)" + fi + + echo " ✓ $label:$apk" + echo " 大小 $((size / 1024)) KiB | $signed" + return 0 +} + +echo "" +log "产物校验:" +FAILED=0 +if [ "$TARGET" = "debug" ] || [ "$TARGET" = "all" ]; then + verify_apk "$DEBUG_APK" "Debug APK" || FAILED=1 +fi +if [ "$TARGET" = "release" ] || [ "$TARGET" = "all" ]; then + verify_apk "$RELEASE_APK" "Release APK" || FAILED=1 +fi + +[ "$FAILED" -eq 0 ] || die "有产物未通过校验" + +echo "" +log "安装到已连接设备:" +echo " adb install -r $DEBUG_APK" +echo " adb install -r $RELEASE_APK" +echo "" +log "查看签名信息:" +echo " \$ANDROID_HOME/build-tools/34.0.0/apksigner verify --print-certs $RELEASE_APK" diff --git a/build/gen-keystore.sh b/build/gen-keystore.sh new file mode 100755 index 0000000..a388d97 --- /dev/null +++ b/build/gen-keystore.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# ============================================================ +# OpenDance Studio —— 生成发布签名密钥库 +# +# 生成的 keystore 与 keystore.properties **不会入库**(见 .gitignore), +# 请自行妥善备份;丢失后无法对已发布 APK 做覆盖安装升级。 +# +# 用法: +# bash build/gen-keystore.sh # 交互式,使用默认文件名 +# CN=yourname bash build/gen-keystore.sh # 指定证书 CN +# KEYSTORE_PASSWORD=xxx KEY_PASSWORD=yyy bash build/gen-keystore.sh +# ============================================================ +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$PROJECT_DIR" + +KEYSTORE_DIR="$PROJECT_DIR/keystore" +KEYSTORE_FILE="$KEYSTORE_DIR/opendance-release.jks" +PROPS_FILE="$PROJECT_DIR/keystore.properties" + +ALIAS="${KEY_ALIAS:-opendance}" +STORE_PASS="${KEYSTORE_PASSWORD:-}" +KEY_PASS="${KEY_PASSWORD:-}" +CN="${CN:-OpenDance Studio}" +DNAME="${DNAME:-CN=$CN, OU=OpenDance, O=OpenDance Studio, L=Unknown, ST=Unknown, C=CN}" +VALIDITY_DAYS="${VALIDITY_DAYS:-10000}" + +if ! command -v keytool >/dev/null 2>&1; then + echo "错误:未找到 keytool。请安装 JDK(17+)并确保 keytool 在 PATH 中。" >&2 + exit 1 +fi + +if [ -f "$KEYSTORE_FILE" ]; then + echo "密钥库已存在:$KEYSTORE_FILE" + echo "如需重新生成,请先删除该文件(注意:会导致已发布 APK 无法升级安装)。" + exit 0 +fi + +if [ -z "$STORE_PASS" ]; then + read -r -s -p "请输入密钥库口令(至少 6 位):" STORE_PASS; echo + read -r -s -p "请再次输入: " STORE_PASS_CONFIRM; echo + [ "$STORE_PASS" = "$STORE_PASS_CONFIRM" ] || { echo "两次输入不一致" >&2; exit 1; } +fi + +if [ -z "$KEY_PASS" ]; then + KEY_PASS="$STORE_PASS" +fi + +if [ ${#STORE_PASS} -lt 6 ]; then + echo "错误:密钥库口令至少 6 位。" >&2 + exit 1 +fi + +mkdir -p "$KEYSTORE_DIR" + +echo "[keystore] 生成 $KEYSTORE_FILE" +keytool -genkeypair \ + -keystore "$KEYSTORE_FILE" \ + -alias "$ALIAS" \ + -keyalg RSA \ + -keysize 2048 \ + -validity "$VALIDITY_DAYS" \ + -storetype PKCS12 \ + -storepass "$STORE_PASS" \ + -keypass "$KEY_PASS" \ + -dname "$DNAME" \ + -v >/dev/null + +cat > "$PROPS_FILE" <&2; exit 1; } + +[ -f "$APK" ] || die "未找到 $APK,请先执行 bash build/build-apk.sh release" + +# APK 是 zip,校验魔数 +MAGIC=$(head -c 2 "$APK" | od -An -tx1 | tr -d ' \n') +[ "$MAGIC" = "504b" ] || die "$APK 不是合法 APK(魔数 $MAGIC)" + +log "整理产物到 $STAGE …" +rm -rf "$STAGE" +mkdir -p "$STAGE" + +cp "$APK" "$STAGE/app-release.apk" +cp THIRD_PARTY.md README.txt "$STAGE/" +cp -r contracts "$STAGE/contracts" + +log "生成压缩包…" +( cd "$OUT_DIR" && zip -qr "$NAME.zip" "$NAME" ) + +log "完成:$OUT_DIR/$NAME.zip($(du -h "$OUT_DIR/$NAME.zip" | cut -f1))" +echo "" +echo "产物内容:" +unzip -l "$OUT_DIR/$NAME.zip" | sed -n '4,20p' +echo "" +echo "分发:把 APK 交给用户(Android 10+),或解压后 adb install -r app-release.apk" diff --git a/build/verify.sh b/build/verify.sh new file mode 100755 index 0000000..c016662 --- /dev/null +++ b/build/verify.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# ============================================================ +# OpenDance Studio —— Android 辅助 App 一键验证 +# +# 依次执行:单元测试 → debug 构建 → release 构建 → 产物校验 +# 任一环节失败即退出非 0。 +# +# 用法: +# JAVA_HOME=/path/to/jdk17 ANDROID_HOME=/path/to/sdk bash build/verify.sh +# ============================================================ +set -uo pipefail + +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$PROJECT_DIR" + +FAILED=0 + +echo "============================================================" +echo " 1/3 单元测试(JVM,无需设备)" +echo "============================================================" +if ./gradlew testDebugUnitTest --console=plain -q 2>&1 | tail -20; then + echo "✓ 单元测试通过" +else + echo "✗ 单元测试失败" + FAILED=1 +fi +echo " 测试报告:app/build/reports/tests/testDebugUnitTest/index.html" + +echo "" +echo "============================================================" +echo " 2/3 构建 debug + release APK" +echo "============================================================" +if bash build/build-apk.sh all 2>&1 | grep -vE "^> Task|^$" | tail -20; then + : +else + FAILED=1 +fi + +echo "" +echo "============================================================" +echo " 3/3 产物校验(签名 / 包信息 / 模型资源)" +echo "============================================================" +SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}" +# 优先选用与 compileSdk 匹配的 build-tools,缺失时退回最新版 +if [ -d "$SDK/build-tools/34.0.0" ]; then + BT="$SDK/build-tools/34.0.0" +else + BT="$(ls -d "$SDK"/build-tools/* 2>/dev/null | sort -V | tail -1)" +fi +RELEASE_APK="app/build/outputs/apk/release/app-release.apk" + +if [ ! -f "$RELEASE_APK" ]; then + echo "✗ 未生成 $RELEASE_APK" + FAILED=1 +elif [ -z "$BT" ]; then + echo "✗ 未找到 build-tools,无法校验" + FAILED=1 +else + # 以**退出码**为准:不同 build-tools 版本的 apksigner 输出文案不一致 + # (34.0.0 会打印 "Verifies",36.0.0 配 --print-certs 时只打印证书信息), + # 依赖日志字符串判断会导致误判。 + "$BT/apksigner" verify "$RELEASE_APK" > /tmp/apk_verify.txt 2>&1 + VERIFY_STATUS=$? + "$BT/apksigner" verify --print-certs "$RELEASE_APK" > /tmp/apk_sign.txt 2>/dev/null + + if [ "$VERIFY_STATUS" -eq 0 ]; then + echo "✓ 签名校验通过(apksigner 退出码 0)" + grep -E "Signer #1 certificate DN" /tmp/apk_sign.txt | sed 's/^/ /' || true + if grep -q "APK Signature Scheme v2" /tmp/apk_verify.txt 2>/dev/null; then + echo " v2 签名方案:已启用" + fi + else + echo "✗ 签名校验失败(apksigner 退出码 $VERIFY_STATUS)" + cat /tmp/apk_verify.txt + FAILED=1 + fi + + "$BT/aapt2" dump badging "$RELEASE_APK" 2>/dev/null | head -1 | sed 's/^/ /' + + if unzip -l "$RELEASE_APK" | grep -q "assets/pose_landmarker_lite.task"; then + echo "✓ MediaPipe 模型已随包分发" + else + echo "✗ APK 中缺少 MediaPipe 模型" + FAILED=1 + fi + + ABIS=$(unzip -l "$RELEASE_APK" | grep -oE "lib/[^/]+/" | sort -u | tr '\n' ' ') + echo "✓ 包含 ABI:$ABIS" +fi + +echo "" +if [ "$FAILED" -ne 0 ]; then + echo "============================================================" + echo " 验证失败" + echo "============================================================" + exit 1 +fi +echo "============================================================" +echo " 验证全部通过" +echo "============================================================" diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..199bf66 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,11 @@ +# OpenDance Studio —— Android 辅助 App 构建配置 + +org.gradle.jvmargs=-Xmx3072m -Dfile.encoding=UTF-8 +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=false + +android.useAndroidX=true +android.nonTransitiveRClass=true + +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..2c35211 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..09523c0 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9b42019 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..ccd55ee --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,24 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "OpenDanceStudio" +include(":app")