From 2d38441ee22f7fc002428708b956f306aec703a2 Mon Sep 17 00:00:00 2001 From: Sarto Date: Tue, 31 Mar 2026 19:29:08 +0330 Subject: [PATCH] feat: update Android client binary staging to use JNI library format --- .github/workflows/build.yml | 9 +-- .../com/thefeed/android/ThefeedService.kt | 70 ++++--------------- 2 files changed, 19 insertions(+), 60 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 62740b8..808240a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,12 +104,13 @@ jobs: path: artifacts merge-multiple: true - - name: Stage Android client binary into assets + - name: Stage Android client binary as JNI library run: | test -f artifacts/thefeed-client-android-arm64 - mkdir -p android/app/src/main/assets - cp artifacts/thefeed-client-android-arm64 android/app/src/main/assets/thefeed-client-arm64 - chmod +x android/app/src/main/assets/thefeed-client-arm64 + # Package binary as .so in jniLibs so the installer places it in + # nativeLibraryDir – the only Android-permitted exec location (W^X policy). + mkdir -p android/app/src/main/jniLibs/arm64-v8a + cp artifacts/thefeed-client-android-arm64 android/app/src/main/jniLibs/arm64-v8a/libthefeed.so - name: Decode signing keystore if: env.KEYSTORE_BASE64 != '' diff --git a/android/app/src/main/java/com/thefeed/android/ThefeedService.kt b/android/app/src/main/java/com/thefeed/android/ThefeedService.kt index a2bd328..c8859ba 100644 --- a/android/app/src/main/java/com/thefeed/android/ThefeedService.kt +++ b/android/app/src/main/java/com/thefeed/android/ThefeedService.kt @@ -11,7 +11,6 @@ import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import java.io.File -import java.io.FileOutputStream import java.net.ServerSocket class ThefeedService : Service() { @@ -64,7 +63,7 @@ class ThefeedService : Service() { Thread { try { - val bin = ensureBinary() + val bin = nativeBin() val dataDir = File(filesDir, "thefeeddata") if (!dataDir.exists()) dataDir.mkdirs() @@ -103,66 +102,25 @@ class ThefeedService : Service() { updateForegroundNotification("Running on http://127.0.0.1:$selectedPort") } catch (e: Exception) { - val detail = (e.message ?: e.javaClass.simpleName) - val abis = Build.SUPPORTED_ABIS.joinToString(",") - val hint = when { - detail.contains("Permission denied", ignoreCase = true) -> - "execution blocked by device policy" - detail.contains("Exec format", ignoreCase = true) || detail.contains("error=8", ignoreCase = true) -> - "ABI mismatch, device ABIs=$abis" - detail.contains("No such file", ignoreCase = true) -> - "binary missing in app assets" - else -> detail - } savePort(-1) - updateForegroundNotification("Failed: $hint") + updateForegroundNotification("Failed: ${e.message ?: e.javaClass.simpleName}") } }.start() } - private fun ensureBinary(): File { - val target = File(filesDir, "thefeed-client") - val selectedAsset = selectAssetByAbi() - - // If already extracted and executable, verify it's still valid - if (target.exists() && target.length() > 0L && target.canExecute()) { - return target + /** + * The Go binary is packaged as libthefeed.so in jniLibs/ so the package + * installer places it in nativeLibraryDir — the only directory Android allows + * execution from (W^X policy blocks exec from filesDir on Android 10+). + */ + private fun nativeBin(): File { + val bin = File(applicationInfo.nativeLibraryDir, "libthefeed.so") + if (!bin.exists()) { + throw IllegalStateException( + "Native binary missing — reinstall the app. Expected: ${bin.absolutePath}" + ) } - - // Extract fresh copy from assets - if (target.exists()) target.delete() - - assets.open(selectedAsset).use { input -> - FileOutputStream(target).use { out -> - input.copyTo(out) - } - } - - if (!target.setExecutable(true, true)) { - throw IllegalStateException("Could not set executable bit on bundled binary") - } - - return target - } - - private fun selectAssetByAbi(): String { - val list = assets.list("")?.toSet() ?: emptySet() - val abis = Build.SUPPORTED_ABIS.map { it.lowercase() } - for (abi in abis) { - val candidate = when (abi) { - "arm64-v8a" -> "thefeed-client-arm64" - "armeabi-v7a" -> "thefeed-client-armv7" - "x86_64" -> "thefeed-client-x86_64" - else -> null - } - if (candidate != null && list.contains(candidate)) { - return candidate - } - } - if (list.contains("thefeed-client")) { - return "thefeed-client" - } - throw IllegalStateException("No compatible binary in assets (device ABIs=${Build.SUPPORTED_ABIS.joinToString(",")})") + return bin } private fun findFreePort(): Int {