From 4f80ea6689b915fe38a2d855bc2fa24919226be5 Mon Sep 17 00:00:00 2001 From: Armand <4831c0@proton.me> Date: Thu, 31 Jul 2025 18:49:22 +0200 Subject: [PATCH] android: transform aabs --- firka/android/app/build.gradle.kts | 81 ++++++++++++++----- .../main/kotlin/app/firka/naplo/AppMain.kt | 47 ++++++++--- 2 files changed, 96 insertions(+), 32 deletions(-) diff --git a/firka/android/app/build.gradle.kts b/firka/android/app/build.gradle.kts index 194caf0c..51bd8e2f 100644 --- a/firka/android/app/build.gradle.kts +++ b/firka/android/app/build.gradle.kts @@ -4,8 +4,8 @@ import java.security.MessageDigest import java.util.Properties import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream -import java.util.zip.ZipOutputStream.STORED import java.util.zip.ZipOutputStream.DEFLATED +import java.util.zip.ZipOutputStream.STORED plugins { id("com.android.application") @@ -105,7 +105,7 @@ flutter { tasks.register("transformAndResignDebugApk") { group = "build" - description = "Transform and resign debug APK with debug key" + description = "Transform and resign APK with debug key" dependsOn("assembleDebug") @@ -116,7 +116,7 @@ tasks.register("transformAndResignDebugApk") { tasks.register("transformAndResignReleaseApk") { group = "build" - description = "Transform and resign debug APK with debug key" + description = "Transform and resign APK with release key" dependsOn("assembleRelease") @@ -125,26 +125,34 @@ tasks.register("transformAndResignReleaseApk") { } } +tasks.register("transformAndResignReleaseBundle") { + group = "build" + description = "Transform and resign bundle with release key" + + dependsOn("bundleRelease") + + doLast { + transformAppBundle() + } +} + afterEvaluate { tasks.findByName("assembleDebug")?.finalizedBy("transformAndResignDebugApk") tasks.findByName("assembleRelease")?.finalizedBy("transformAndResignReleaseApk") + tasks.findByName("bundleRelease")?.finalizedBy("transformAndResignReleaseBundle") } fun transformApks(debug: Boolean) { - val buildDir = project.buildDir - val apkDir = File(buildDir, "outputs/flutter-apk") - val apks = apkDir.listFiles()!! - val flavor = if (debug) { "debug" } else { "release" } - println("Starting APK transformation process...") + val buildDir = project.buildDir + val apkDir = File(buildDir, "outputs/flutter-apk") + val apks = getApks(debug) var c = 0; apks - .filter { apk -> apk.name.startsWith("app-") && apk.name.endsWith("-$flavor.apk") } .forEach { c++; transformAndSignApk(apkDir, it.nameWithoutExtension, debug) } println("Transformed: $c apks") - } fun transformAndSignApk(apkDir: File, name: String, debug: Boolean) { @@ -201,8 +209,6 @@ fun transformApk(input: File, output: File, compressionLevel: String = "Z") { into(tempDir) } - val assetsDir = File(tempDir, "assets") - val metaInf = File(tempDir, "META-INF") val metaInfFiles = metaInf.listFiles(); for (file in metaInfFiles!!) { @@ -216,16 +222,12 @@ fun transformApk(input: File, output: File, compressionLevel: String = "Z") { val compressedLibs = mutableMapOf() for (arch in arches!!) { val libFlutter = File(arch, "libflutter.so") - val libApp = File(arch, "libapp.so") if (!libFlutter.exists()) continue - val compressedDir = File(assetsDir, "flutter-br-${arch.name}") - val compressedFlutter = File(compressedDir, "libflutter.so.br") + val compressedFlutter = File(arch, "libflutter-br.so") - if (!compressedDir.exists()) compressedDir.mkdirs() - - compressedLibs["${arch.name}/libflutter.so"] = libFlutter.sha256() + compressedLibs["libflutter.so"] = libFlutter.sha256() println("Compressing ${arch.name}/libflutter.so with brotli") exec { @@ -237,10 +239,10 @@ fun transformApk(input: File, output: File, compressionLevel: String = "Z") { ) } libFlutter.delete() - } - val json = groovy.json.JsonBuilder(compressedLibs) - File(assetsDir, "flutter-br.json").writeText(json.toString()) + val json = groovy.json.JsonBuilder(compressedLibs) + File(arch, "index.so").writeText(json.toString()) + } val topDirL = tempDir.absolutePath.length + 1 val zos = ZipOutputStream(output.outputStream()); @@ -275,12 +277,49 @@ fun transformApk(input: File, output: File, compressionLevel: String = "Z") { println("APK transformed successfully") } +fun transformAppBundle() { + val buildDir = project.buildDir + val bundle = File(buildDir, "outputs/bundle/release/app-release.aab") + + val apks = getApks(false) + val apkCount = apks.count { it.name.startsWith("app-") && it.name.endsWith("-release.apk") } + + if (!bundle.exists()) { + throw Exception("Bundle not found at: $bundle") + } + + if (apkCount < 3) { + throw Exception("Excepected 3 apks per abi but only found $apkCount") + } + + val aabTempDir = File(project.buildDir, "tmp/aab-transform") + aabTempDir.deleteRecursively() + aabTempDir.mkdirs() + + copy { + from(zipTree(bundle)) + into(aabTempDir) + } + +} + fun File.sha256(): String { val md = MessageDigest.getInstance("SHA-256") val digest = md.digest(this.readBytes()) return digest.fold("") { str, it -> str + "%02x".format(it) } } +fun getApks(debug: Boolean): List { + val buildDir = project.buildDir + val apkDir = File(buildDir, "outputs/flutter-apk") + val apks = apkDir.listFiles()!! + val flavor = if (debug) { "debug" } else { "release" } + + return apks + .filter { apk -> apk.name.startsWith("app-") && apk.name.endsWith("-$flavor.apk") } + .toList() +} + fun getDebugKeystorePath(): String { val userHome = System.getProperty("user.home") val debugKeystore = File(userHome, ".android/debug.keystore") diff --git a/firka/android/app/src/main/kotlin/app/firka/naplo/AppMain.kt b/firka/android/app/src/main/kotlin/app/firka/naplo/AppMain.kt index 9abe0e43..6a4ad1b1 100644 --- a/firka/android/app/src/main/kotlin/app/firka/naplo/AppMain.kt +++ b/firka/android/app/src/main/kotlin/app/firka/naplo/AppMain.kt @@ -5,11 +5,11 @@ import android.app.Application import android.os.Build import android.util.Log import org.brotli.dec.BrotliInputStream -import org.json.JSONArray import org.json.JSONObject import java.io.File import java.io.FileOutputStream import java.security.MessageDigest +import java.util.zip.ZipFile class AppMain : Application() { @@ -25,29 +25,54 @@ class AppMain : Application() { override fun onCreate() { super.onCreate() - val am = assets val abi = Build.SUPPORTED_ABIS[0] - val assets = am.list("") - if (!assets?.contains("flutter-br-$abi")!!) { - throw Exception("Unsupported abi: $abi, try downloading an apk with a different abi") + val apks = File(applicationInfo.nativeLibraryDir, "../..").absoluteFile + .listFiles()!! + .filter { file -> file.name.endsWith(".apk") } + .toList() + + var nativesApkN: ZipFile? = null + for (apk in apks) { + if (nativesApkN != null) break + + val zip = ZipFile(apk) + val entries = zip.entries() + + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + + entry.name.endsWith("$abi/index.so") + zip.close() + nativesApkN = ZipFile(apk) + break + } + + zip.close() } - val compressedLibsIndex = am.open("flutter-br.json") + if (nativesApkN == null) { + throw Exception("Can't find native libraries") + } + val nativesApk: ZipFile = nativesApkN + + val compressedLibsIndex = nativesApk.getInputStream( + nativesApk.getEntry("lib/$abi/index.so") + ) val compressedLibs = JSONObject(compressedLibsIndex.readBytes().toString(Charsets.UTF_8)) - val natives = am.list("flutter-br-$abi")!! - for (lib in natives) { - val so = lib.substring(0, lib.length-".br".length) + for (so in compressedLibs.keys()) { val soFile = File(cacheDir, so) - if (soFile.sha256() == compressedLibs.getString("${abi}/$so")) { + if (soFile.sha256() == compressedLibs.getString(so)) { System.load(soFile.absolutePath) return } Log.d("AppMain", "Decompressing: $so") - val brInput = am.open("flutter-br-$abi/$lib") + val brInput = nativesApk.getInputStream( + nativesApk.getEntry("lib/$abi/${so.replace(".so", "-br.so")}") + ) val soOutput = FileOutputStream(soFile) val brIn = BrotliInputStream(brInput)