build: Android APK builds working — debug (8.9MB) and release (2.0MB)

- Fix C++ std::std:: double namespace in oboe_bridge.cpp
- Auto-fetch Oboe headers from GitHub in build.rs
- Configure cargo cross-compilation (.cargo/config.toml) with NDK linkers
- Fix Gradle settings (dependencyResolutionManagement), signing configs,
  Compose LinearProgressIndicator API, and Android manifest theme
- Add Gradle wrapper, .gitignore for build artifacts
- arm64-v8a only (raptorq crate incompatible with armv7 32-bit)
- Release APK: 2.0MB signed with wzp-release key
- Debug APK: 8.9MB signed with wzp-debug key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude
2026-04-04 19:37:08 +00:00
parent e7b1c3372a
commit 73ebcdd869
11 changed files with 123 additions and 25 deletions

5
.cargo/config.toml Normal file
View File

@@ -0,0 +1,5 @@
[target.aarch64-linux-android]
linker = "aarch64-linux-android21-clang"
[target.armv7-linux-androideabi]
linker = "armv7a-linux-androideabi21-clang"

6
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.gradle/
build/
app/build/
app/src/main/jniLibs/
local.properties
keystore/*.jks

View File

@@ -13,11 +13,31 @@ android {
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "0.1.0" versionName = "0.1.0"
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") } ndk { abiFilters += listOf("arm64-v8a") }
}
signingConfigs {
create("release") {
storeFile = file("${project.rootDir}/keystore/wzp-release.jks")
storePassword = "wzphone2024"
keyAlias = "wzp-release"
keyPassword = "wzphone2024"
}
getByName("debug") {
storeFile = file("${project.rootDir}/keystore/wzp-debug.jks")
storePassword = "android"
keyAlias = "wzp-debug"
keyPassword = "android"
}
} }
buildTypes { buildTypes {
debug {
signingConfig = signingConfigs.getByName("debug")
isDebuggable = true
}
release { release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true isMinifyEnabled = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
@@ -46,7 +66,7 @@ tasks.register<Exec>("cargoNdkBuild") {
workingDir = file("${project.rootDir}/..") workingDir = file("${project.rootDir}/..")
commandLine( commandLine(
"cargo", "ndk", "cargo", "ndk",
"-t", "arm64-v8a", "-t", "armeabi-v7a", "-t", "arm64-v8a",
"-o", "${project.projectDir}/src/main/jniLibs", "-o", "${project.projectDir}/src/main/jniLibs",
"build", "--release", "-p", "wzp-android" "build", "--release", "-p", "wzp-android"
) )

View File

@@ -13,7 +13,7 @@
android:name=".WzpApplication" android:name=".WzpApplication"
android:label="WZ Phone" android:label="WZ Phone"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Material3.DayNight"> android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity <activity
android:name=".ui.call.CallActivity" android:name=".ui.call.CallActivity"

View File

@@ -186,7 +186,7 @@ private fun AudioLevelBar(framesEncoded: Long) {
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
LinearProgressIndicator( LinearProgressIndicator(
progress = { level }, progress = level,
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.6f) .fillMaxWidth(0.6f)
.height(6.dp) .height(6.dp)

Binary file not shown.

View File

@@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

5
android/gradlew vendored Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
# Gradle wrapper script
APP_HOME=$(cd "$(dirname "$0")" && pwd)
CLASSPATH="$APP_HOME/gradle/wrapper/gradle-wrapper.jar"
exec java -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

View File

@@ -6,7 +6,8 @@ pluginManagement {
} }
} }
dependencyResolution { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
google() google()
mavenCentral() mavenCentral()

View File

@@ -1,15 +1,36 @@
use std::path::PathBuf;
fn main() { fn main() {
let target = std::env::var("TARGET").unwrap_or_default(); let target = std::env::var("TARGET").unwrap_or_default();
if target.contains("android") { if target.contains("android") {
// Real Oboe build for Android targets // On Android, try to build with Oboe. If Oboe is not available,
cc::Build::new() // fall back to the stub (audio will need to be provided via JNI).
.cpp(true) let oboe_dir = fetch_oboe();
.std("c++17") match oboe_dir {
.file("cpp/oboe_bridge.cpp") Some(oboe_path) => {
.include("cpp") println!("cargo:warning=Building with Oboe from {:?}", oboe_path);
.compile("oboe_bridge"); cc::Build::new()
.cpp(true)
.std("c++17")
.file("cpp/oboe_bridge.cpp")
.include("cpp")
.include(oboe_path.join("include"))
.define("WZP_HAS_OBOE", None)
.compile("oboe_bridge");
}
None => {
println!("cargo:warning=Oboe not found, building with stub");
cc::Build::new()
.cpp(true)
.std("c++17")
.file("cpp/oboe_stub.cpp")
.include("cpp")
.compile("oboe_bridge");
}
}
} else { } else {
// Stub for host builds / testing // Non-Android: always use stub
cc::Build::new() cc::Build::new()
.cpp(true) .cpp(true)
.std("c++17") .std("c++17")
@@ -18,3 +39,37 @@ fn main() {
.compile("oboe_bridge"); .compile("oboe_bridge");
} }
} }
/// Try to find or fetch Oboe headers.
/// Returns the path to the Oboe source root (containing include/ directory).
fn fetch_oboe() -> Option<PathBuf> {
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
let oboe_dir = out_dir.join("oboe");
// Check if already fetched
if oboe_dir.join("include").join("oboe").join("Oboe.h").exists() {
return Some(oboe_dir);
}
// Try to clone Oboe from GitHub
let status = std::process::Command::new("git")
.args([
"clone",
"--depth=1",
"--branch=1.8.1",
"https://github.com/google/oboe.git",
oboe_dir.to_str().unwrap(),
])
.status();
match status {
Ok(s) if s.success() => {
if oboe_dir.join("include").join("oboe").join("Oboe.h").exists() {
Some(oboe_dir)
} else {
None
}
}
_ => None,
}
}

View File

@@ -79,7 +79,7 @@ public:
oboe::AudioStream* stream, oboe::AudioStream* stream,
void* audioData, void* audioData,
int32_t numFrames) override { int32_t numFrames) override {
if (!g_running.load(std::std::memory_order_relaxed) || !g_rings) { if (!g_running.load(std::memory_order_relaxed) || !g_rings) {
return oboe::DataCallbackResult::Stop; return oboe::DataCallbackResult::Stop;
} }
@@ -98,7 +98,7 @@ public:
auto result = stream->calculateLatencyMillis(); auto result = stream->calculateLatencyMillis();
if (result) { if (result) {
g_capture_latency_ms.store(static_cast<float>(result.value()), g_capture_latency_ms.store(static_cast<float>(result.value()),
std::std::memory_order_relaxed); std::memory_order_relaxed);
} }
return oboe::DataCallbackResult::Continue; return oboe::DataCallbackResult::Continue;
@@ -115,7 +115,7 @@ public:
oboe::AudioStream* stream, oboe::AudioStream* stream,
void* audioData, void* audioData,
int32_t numFrames) override { int32_t numFrames) override {
if (!g_running.load(std::std::memory_order_relaxed) || !g_rings) { if (!g_running.load(std::memory_order_relaxed) || !g_rings) {
memset(audioData, 0, numFrames * sizeof(int16_t)); memset(audioData, 0, numFrames * sizeof(int16_t));
return oboe::DataCallbackResult::Stop; return oboe::DataCallbackResult::Stop;
} }
@@ -140,7 +140,7 @@ public:
auto result = stream->calculateLatencyMillis(); auto result = stream->calculateLatencyMillis();
if (result) { if (result) {
g_playout_latency_ms.store(static_cast<float>(result.value()), g_playout_latency_ms.store(static_cast<float>(result.value()),
std::std::memory_order_relaxed); std::memory_order_relaxed);
} }
return oboe::DataCallbackResult::Continue; return oboe::DataCallbackResult::Continue;
@@ -155,7 +155,7 @@ static PlayoutCallback g_playout_cb;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) { int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) {
if (g_running.load(std::std::memory_order_relaxed)) { if (g_running.load(std::memory_order_relaxed)) {
LOGW("wzp_oboe_start: already running"); LOGW("wzp_oboe_start: already running");
return -1; return -1;
} }
@@ -200,13 +200,13 @@ int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) {
return -3; return -3;
} }
g_running.store(true, std::std::memory_order_release); g_running.store(true, std::memory_order_release);
// Start both streams // Start both streams
result = g_capture_stream->requestStart(); result = g_capture_stream->requestStart();
if (result != oboe::Result::OK) { if (result != oboe::Result::OK) {
LOGE("Failed to start capture: %s", oboe::convertToText(result)); LOGE("Failed to start capture: %s", oboe::convertToText(result));
g_running.store(false, std::std::memory_order_release); g_running.store(false, std::memory_order_release);
g_capture_stream->close(); g_capture_stream->close();
g_playout_stream->close(); g_playout_stream->close();
g_capture_stream.reset(); g_capture_stream.reset();
@@ -217,7 +217,7 @@ int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) {
result = g_playout_stream->requestStart(); result = g_playout_stream->requestStart();
if (result != oboe::Result::OK) { if (result != oboe::Result::OK) {
LOGE("Failed to start playout: %s", oboe::convertToText(result)); LOGE("Failed to start playout: %s", oboe::convertToText(result));
g_running.store(false, std::std::memory_order_release); g_running.store(false, std::memory_order_release);
g_capture_stream->requestStop(); g_capture_stream->requestStop();
g_capture_stream->close(); g_capture_stream->close();
g_playout_stream->close(); g_playout_stream->close();
@@ -232,7 +232,7 @@ int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) {
} }
void wzp_oboe_stop(void) { void wzp_oboe_stop(void) {
g_running.store(false, std::std::memory_order_release); g_running.store(false, std::memory_order_release);
if (g_capture_stream) { if (g_capture_stream) {
g_capture_stream->requestStop(); g_capture_stream->requestStop();
@@ -250,15 +250,15 @@ void wzp_oboe_stop(void) {
} }
float wzp_oboe_capture_latency_ms(void) { float wzp_oboe_capture_latency_ms(void) {
return g_capture_latency_ms.load(std::std::memory_order_relaxed); return g_capture_latency_ms.load(std::memory_order_relaxed);
} }
float wzp_oboe_playout_latency_ms(void) { float wzp_oboe_playout_latency_ms(void) {
return g_playout_latency_ms.load(std::std::memory_order_relaxed); return g_playout_latency_ms.load(std::memory_order_relaxed);
} }
int wzp_oboe_is_running(void) { int wzp_oboe_is_running(void) {
return g_running.load(std::std::memory_order_relaxed) ? 1 : 0; return g_running.load(std::memory_order_relaxed) ? 1 : 0;
} }
#else #else