From 88350c0c146bcebf2e92c9c059c02d0bb892c5da Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:16:45 +0200 Subject: [PATCH 1/3] Update WebRTC Android dependency for 16KB page-size compatibility --- app/build.gradle.kts | 2 +- humanoperator/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6a69feb..12e6cc9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -159,7 +159,7 @@ dependencies { implementation("androidx.camera:camera-core:1.4.0") // WebRTC - implementation("io.getstream:stream-webrtc-android:1.1.1") + implementation("io.getstream:stream-webrtc-android:1.3.10") // WebSocket for signaling implementation("com.squareup.okhttp3:okhttp:4.12.0") diff --git a/humanoperator/build.gradle.kts b/humanoperator/build.gradle.kts index 40ee702..44c78df 100644 --- a/humanoperator/build.gradle.kts +++ b/humanoperator/build.gradle.kts @@ -93,7 +93,7 @@ dependencies { implementation("androidx.compose.material:material-icons-extended") // WebRTC - implementation("io.getstream:stream-webrtc-android:1.1.1") + implementation("io.getstream:stream-webrtc-android:1.3.10") // WebSocket for signaling implementation("com.squareup.okhttp3:okhttp:4.12.0") From 5a2abde3a415b8b8c1553f4ef053d1af0620d24e Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:36:24 +0200 Subject: [PATCH 2/3] Update camera-core dependency in Screen Operator --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 12e6cc9..4450f2a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -156,7 +156,7 @@ dependencies { implementation("com.google.ai.edge.litertlm:litertlm-android:0.10.0") // Camera Core to potentially fix missing JNI lib issue - implementation("androidx.camera:camera-core:1.4.0") + implementation("androidx.camera:camera-core:1.4.2") // WebRTC implementation("io.getstream:stream-webrtc-android:1.3.10") From bf763c1eb08360ef695fb4866f4addefa8e884ef Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:20:14 +0200 Subject: [PATCH 3/3] Enforce 16KB native alignment checks on Android builds --- app/build.gradle.kts | 91 ++++++++++++++++++++++++++++++++- humanoperator/build.gradle.kts | 92 ++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4450f2a..d0a0418 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.io.ByteArrayOutputStream + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") @@ -29,6 +31,7 @@ val isReleaseTaskRequested = gradle.startParameter.taskNames.any { task -> } val missingReleaseSigningEnvText = missingReleaseSigningEnv.joinToString(separator = ", ") +val supportedAbis = listOf("arm64-v8a", "x86_64") android { namespace = "com.google.ai.sample" @@ -41,7 +44,7 @@ android { versionCode = 1 versionName = "1.0" ndk { - abiFilters += listOf("arm64-v8a", "x86_64") + abiFilters += supportedAbis } testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -93,6 +96,92 @@ android { composeOptions { kotlinCompilerExtensionVersion = "1.5.4" } + packaging { + jniLibs { + useLegacyPackaging = false + } + } +} + +fun parseLoadAlignments(readelfOutput: String): List { + val lines = readelfOutput.lineSequence().toList() + val alignments = mutableListOf() + for (index in 0 until lines.lastIndex) { + if (!lines[index].trimStart().startsWith("LOAD")) continue + val alignToken = lines[index + 1].trim().split(Regex("\\s+")).lastOrNull() ?: continue + val alignValue = alignToken.removePrefix("0x").toLongOrNull(16) ?: continue + alignments += alignValue + } + return alignments +} + +androidComponents { + onVariants(selector().all()) { variant -> + val variantName = variant.name + val variantNameCap = variantName.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + val mergeNativeTaskName = "merge${variantNameCap}NativeLibs" + val verifyTaskName = "verify${variantNameCap}Native16KbAlignment" + + val verifyTask = tasks.register(verifyTaskName) { + group = "verification" + description = "Verifies that all merged native libs for $variantName use at least 16KB PT_LOAD alignment." + dependsOn(mergeNativeTaskName) + + doLast { + val nativeOutDir = layout.buildDirectory + .dir("intermediates/merged_native_libs/$variantName/$mergeNativeTaskName/out/lib") + .get() + .asFile + + if (!nativeOutDir.exists()) { + throw GradleException("Native lib output directory not found: ${nativeOutDir.absolutePath}") + } + + val soFiles = nativeOutDir.walkTopDown().filter { it.isFile && it.extension == "so" }.toList() + val filteredSoFiles = soFiles.filter { soFile -> + val abiDir = soFile.parentFile?.name + abiDir in supportedAbis + } + if (filteredSoFiles.isEmpty()) { + logger.lifecycle("No native .so files found under ${nativeOutDir.absolutePath} for variant $variantName.") + return@doLast + } + + val invalidLibraries = mutableListOf() + filteredSoFiles.forEach { soFile -> + val stdout = ByteArrayOutputStream() + val execResult = exec { + commandLine("readelf", "-l", soFile.absolutePath) + standardOutput = stdout + isIgnoreExitValue = false + } + if (execResult.exitValue != 0) { + throw GradleException("readelf failed for ${soFile.absolutePath}") + } + + val alignments = parseLoadAlignments(stdout.toString()) + if (alignments.isEmpty() || alignments.any { it < 0x4000L }) { + val relativePath = soFile.relativeTo(nativeOutDir).path + val shownAlignments = if (alignments.isEmpty()) "none" else alignments.joinToString(", ") { "0x${it.toString(16)}" } + invalidLibraries += "$relativePath (PT_LOAD alignments: $shownAlignments)" + } + } + + if (invalidLibraries.isNotEmpty()) { + throw GradleException( + "Found native libraries without required 16KB alignment in variant '$variantName':\n" + + invalidLibraries.joinToString("\n") + ) + } + } + } + + tasks.configureEach { + if (name == "assemble$variantNameCap") { + dependsOn(verifyTask) + } + } + } } if (isReleaseTaskRequested && missingReleaseSigningEnv.isNotEmpty()) { diff --git a/humanoperator/build.gradle.kts b/humanoperator/build.gradle.kts index 44c78df..cad52c1 100644 --- a/humanoperator/build.gradle.kts +++ b/humanoperator/build.gradle.kts @@ -1,3 +1,5 @@ +import java.io.ByteArrayOutputStream + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") @@ -20,6 +22,7 @@ val isReleaseTaskRequested = gradle.startParameter.taskNames.any { task -> } val missingReleaseSigningEnvText = missingReleaseSigningEnv.joinToString(separator = ", ") +val supportedAbis = listOf("arm64-v8a", "x86_64") android { namespace = "com.screenoperator.humanoperator" @@ -31,6 +34,9 @@ android { targetSdk = 35 versionCode = 1 versionName = "1.0" + ndk { + abiFilters += supportedAbis + } testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -70,6 +76,92 @@ android { composeOptions { kotlinCompilerExtensionVersion = "1.5.4" } + packaging { + jniLibs { + useLegacyPackaging = false + } + } +} + +fun parseLoadAlignments(readelfOutput: String): List { + val lines = readelfOutput.lineSequence().toList() + val alignments = mutableListOf() + for (index in 0 until lines.lastIndex) { + if (!lines[index].trimStart().startsWith("LOAD")) continue + val alignToken = lines[index + 1].trim().split(Regex("\\s+")).lastOrNull() ?: continue + val alignValue = alignToken.removePrefix("0x").toLongOrNull(16) ?: continue + alignments += alignValue + } + return alignments +} + +androidComponents { + onVariants(selector().all()) { variant -> + val variantName = variant.name + val variantNameCap = variantName.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + val mergeNativeTaskName = "merge${variantNameCap}NativeLibs" + val verifyTaskName = "verify${variantNameCap}Native16KbAlignment" + + val verifyTask = tasks.register(verifyTaskName) { + group = "verification" + description = "Verifies that all merged native libs for $variantName use at least 16KB PT_LOAD alignment." + dependsOn(mergeNativeTaskName) + + doLast { + val nativeOutDir = layout.buildDirectory + .dir("intermediates/merged_native_libs/$variantName/$mergeNativeTaskName/out/lib") + .get() + .asFile + + if (!nativeOutDir.exists()) { + throw GradleException("Native lib output directory not found: ${nativeOutDir.absolutePath}") + } + + val soFiles = nativeOutDir.walkTopDown().filter { it.isFile && it.extension == "so" }.toList() + val filteredSoFiles = soFiles.filter { soFile -> + val abiDir = soFile.parentFile?.name + abiDir in supportedAbis + } + if (filteredSoFiles.isEmpty()) { + logger.lifecycle("No native .so files found under ${nativeOutDir.absolutePath} for variant $variantName.") + return@doLast + } + + val invalidLibraries = mutableListOf() + filteredSoFiles.forEach { soFile -> + val stdout = ByteArrayOutputStream() + val execResult = exec { + commandLine("readelf", "-l", soFile.absolutePath) + standardOutput = stdout + isIgnoreExitValue = false + } + if (execResult.exitValue != 0) { + throw GradleException("readelf failed for ${soFile.absolutePath}") + } + + val alignments = parseLoadAlignments(stdout.toString()) + if (alignments.isEmpty() || alignments.any { it < 0x4000L }) { + val relativePath = soFile.relativeTo(nativeOutDir).path + val shownAlignments = if (alignments.isEmpty()) "none" else alignments.joinToString(", ") { "0x${it.toString(16)}" } + invalidLibraries += "$relativePath (PT_LOAD alignments: $shownAlignments)" + } + } + + if (invalidLibraries.isNotEmpty()) { + throw GradleException( + "Found native libraries without required 16KB alignment in variant '$variantName':\n" + + invalidLibraries.joinToString("\n") + ) + } + } + } + + tasks.configureEach { + if (name == "assemble$variantNameCap") { + dependsOn(verifyTask) + } + } + } } if (isReleaseTaskRequested && missingReleaseSigningEnv.isNotEmpty()) {