import java.util.Properties import java.io.FileInputStream import java.security.MessageDigest import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.FileSystemOperations import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import javax.inject.Inject plugins { id("com.android.application") id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") id("com.google.gms.google-services") apply false } if (file("google-services.json").exists()) { apply(plugin = "com.google.gms.google-services") } val keystoreProperties = Properties() val keystorePropertiesFile = rootProject.file( providers.gradleProperty("releaseSigningProperties").getOrElse("key.properties") ) val hasReleaseSigning = keystorePropertiesFile.exists() if (hasReleaseSigning) { keystoreProperties.load(FileInputStream(keystorePropertiesFile)) } gradle.taskGraph.whenReady { if (allTasks.any { it.project == project && it.name.contains("Release", ignoreCase = true) }) { check(hasReleaseSigning) { "Release signing requires android/key.properties" } for (key in listOf("keyAlias", "keyPassword", "storeFile", "storePassword")) { check(!keystoreProperties.getProperty(key).isNullOrBlank()) { "Missing release signing property: $key" } } check(file(keystoreProperties.getProperty("storeFile")).isFile) { "Release keystore does not exist" } } } android { namespace = "ru.ikar.app" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion compileOptions { isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } defaultConfig { applicationId = "ru.ikar.app" minSdk = maxOf(flutter.minSdkVersion, 23) targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName } flavorDimensions += "distribution" productFlavors { create("ikar") { dimension = "distribution" isDefault = true applicationId = "ru.ikar.app" } } signingConfigs { if (hasReleaseSigning) { create("release") { keyAlias = keystoreProperties["keyAlias"] as String keyPassword = keystoreProperties["keyPassword"] as String storeFile = file(keystoreProperties["storeFile"] as String) storePassword = keystoreProperties["storePassword"] as String } } } buildTypes { release { signingConfig = if (hasReleaseSigning) { signingConfigs.getByName("release") } else null } } packaging { jniLibs { useLegacyPackaging = true } } } flutter { source = "../.." } abstract class StageFlutterJni : DefaultTask() { @get:InputDirectory abstract val inputDirectory: DirectoryProperty @get:OutputDirectory abstract val outputDirectory: DirectoryProperty @get:Inject abstract val fileSystem: FileSystemOperations @TaskAction fun stage() { fileSystem.sync { from(inputDirectory) into(outputDirectory) } } } androidComponents { onVariants(selector().withBuildType("release")) { variant -> val variantName = variant.name val capitalized = variantName.replaceFirstChar { it.uppercase() } val stage = tasks.register("stage${capitalized}FlutterJni") { dependsOn("copyJniLibsflutterBuild$capitalized") inputDirectory.set(layout.buildDirectory.dir("intermediates/flutter/$variantName/jniLibs")) outputDirectory.set(layout.buildDirectory.dir("intermediates/qlyra_flutter_jni/$variantName")) } variant.sources.jniLibs?.addGeneratedSourceDirectory(stage) { it.outputDirectory } } } android.applicationVariants.all { if (buildType.name == "release") { val variantName = name val capitalized = variantName.replaceFirstChar { it.uppercase() } val generated = layout.buildDirectory.dir("intermediates/flutter/$variantName/jniLibs") val merged = layout.buildDirectory.dir( "intermediates/merged_native_libs/$variantName/merge${capitalized}NativeLibs/out/lib" ) val verifyJni = tasks.register("verify${capitalized}FlutterJni") { dependsOn("merge${capitalized}NativeLibs", "copyJniLibsflutterBuild$capitalized") doLast { val root = generated.get().asFile val libraries = root.walkTopDown().filter { it.isFile && it.extension == "so" }.toList() check(libraries.any { it.name == "libapp.so" }) { "Compiled Flutter library is missing" } for (appLibrary in libraries.filter { it.name == "libapp.so" }) { val abi = appLibrary.parentFile.name val runtime = merged.get().asFile.resolve("$abi/libfjs.so") check(runtime.isFile && runtime.length() > 0) { "Plugin runtime is missing for $abi; inspect the fjs/cargokit build log" } val crypto = merged.get().asFile.resolve("$abi/libkomet_crypto.so") check(crypto.isFile && crypto.length() > 0) { "E2EE crypto library is missing for $abi" } val renderer = merged.get().asFile.resolve("$abi/libkomet_tlottie.so") check(renderer.isFile && renderer.length() > 0) { "Tlottie renderer is missing for $abi; inspect the cargokit build log" } } for (library in libraries) { val relative = library.relativeTo(root) val packaged = merged.get().asFile.resolve(relative) check(packaged.isFile) { "Flutter library was not merged: $relative" } val expected = MessageDigest.getInstance("SHA-256").digest(library.readBytes()) val actual = MessageDigest.getInstance("SHA-256").digest(packaged.readBytes()) check(expected.contentEquals(actual)) { "Stale Flutter library in package: $relative" } } logger.lifecycle("Verified fresh Flutter JNI for $variantName (${libraries.size} libraries)") } } tasks.matching { it.name == "strip${capitalized}DebugSymbols" }.configureEach { dependsOn(verifyJni) } } } dependencies { testImplementation("junit:junit:4.13.2") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") implementation("androidx.media3:media3-transformer:1.9.3") implementation("androidx.media3:media3-effect:1.9.3") implementation("androidx.media3:media3-common:1.9.3") implementation(platform("com.google.firebase:firebase-bom:33.7.0")) implementation("com.google.firebase:firebase-messaging") implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.7.1") }