import java.io.File import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.screenshot) } // The Rust core (rust-core/, over oxy-core) is built for the app's ABIs by // cargo-ndk and its Kotlin bindings generated by uniffi at build time // (oxygenhub-android#1): nothing built is kept in the repository. val rustCore: File = rootProject.file("rust-core") val rustJniLibs = layout.buildDirectory.dir("rustJniLibs") val rustBindings = layout.buildDirectory.dir("generated/uniffi/kotlin") fun androidNdk(): String { System.getenv("ANDROID_NDK_HOME")?.let { return it } val props = Properties() val local = rootProject.file("local.properties") if (local.exists()) local.inputStream().use { input -> props.load(input) } val sdk = props.getProperty("sdk.dir") ?: System.getenv("ANDROID_HOME") ?: error("set sdk.dir in local.properties or ANDROID_HOME") val ndks = File(sdk, "ndk").listFiles()?.filter { f -> f.isDirectory }?.sortedBy { f -> f.name }.orEmpty() return ndks.lastOrNull()?.path ?: error("no NDK under $sdk/ndk: install one with the SDK manager") } val buildRustCore = tasks.register("buildRustCore", Exec::class) { description = "Builds rust-core for arm64-v8a and x86_64 with cargo-ndk." workingDir = rustCore inputs.dir(rustCore.resolve("src")) inputs.files(rustCore.resolve("Cargo.toml"), rustCore.resolve("Cargo.lock")) // The runner's Rust (OxygenFlow and its oxy-core) is built in too. inputs.files(fileTree(rootProject.file("oxygenflow")) { include("**/*.rs", "**/Cargo.toml"); exclude("**/target/**") }) outputs.dir(rustJniLibs) environment("ANDROID_NDK_HOME", androidNdk()) commandLine("cargo", "ndk", "-t", "arm64-v8a", "-t", "x86_64", "--platform", "31", "-o", rustJniLibs.get().asFile.path, "build", "--release") } val generateRustBindings = tasks.register("generateRustBindings", Exec::class) { description = "Generates rust-core's Kotlin bindings with uniffi." dependsOn(buildRustCore) workingDir = rustCore inputs.dir(rustJniLibs) inputs.file(rustCore.resolve("uniffi.toml")) outputs.dir(rustBindings) commandLine( "cargo", "run", "--release", "--bin", "uniffi-bindgen", "generate", "--library", rustJniLibs.get().file("arm64-v8a/librust_core.so").asFile.path, "--language", "kotlin", "--out-dir", rustBindings.get().asFile.path, ) } tasks.named("preBuild") { dependsOn(generateRustBindings) } // The Firebase client settings alerts need (#16), read from // app/google-services.json without Google's Gradle plugin: none, no alerts. val firebase: Map = run { val file = File(projectDir, "google-services.json") if (!file.isFile) return@run emptyMap() @Suppress("UNCHECKED_CAST") val json = groovy.json.JsonSlurper().parse(file) as Map val project = json["project_info"] as Map val client = (json["client"] as List>).first { ((it["client_info"] as Map)["android_client_info"] as Map)["package_name"] == "io.oxygenhub.android" } mapOf( "app_id" to (client["client_info"] as Map)["mobilesdk_app_id"].toString(), "project_id" to project["project_id"].toString(), "sender_id" to project["project_number"].toString(), "api_key" to ((client["api_key"] as List>).first()["current_key"]).toString(), ) } // The version, from gradle.properties (#7). val appVersion: String = providers.gradleProperty("oxygenhub.version").get() val appVersionCode: Int = appVersion.split(".").map { it.toInt() }.let { (a, b, c) -> a * 10000 + b * 100 + c } // Release signing (#7): the release job decodes its keystore secret to a // file and names it and the password's file here. Without them a release // build is unsigned, so nothing else needs the key. val releaseKeystore: File? = providers.environmentVariable("OXYGENHUB_KEYSTORE").orNull?.let { File(it) }?.takeIf { it.isFile } val releasePassword: String? = providers.environmentVariable("OXYGENHUB_KEYSTORE_PASSWORD_FILE").orNull?.let { File(it).readText().trim() } android { namespace = "io.oxygenhub.android" compileSdk { version = release(37) } defaultConfig { applicationId = "io.oxygenhub.android" minSdk = 31 targetSdk = 37 versionCode = appVersionCode versionName = appVersion testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // Firebase for alerts (#16), from google-services.json when the // build has one (the release job writes it from a secret); empty, // the app takes no alerts. buildConfigField("String", "FIREBASE_APP_ID", "\"${firebase["app_id"].orEmpty()}\"") buildConfigField("String", "FIREBASE_PROJECT_ID", "\"${firebase["project_id"].orEmpty()}\"") buildConfigField("String", "FIREBASE_SENDER_ID", "\"${firebase["sender_id"].orEmpty()}\"") buildConfigField("String", "FIREBASE_API_KEY", "\"${firebase["api_key"].orEmpty()}\"") } signingConfigs { if (releaseKeystore != null && releasePassword != null) { create("release") { storeFile = releaseKeystore storeType = "PKCS12" storePassword = releasePassword keyAlias = "oxygenhub" keyPassword = releasePassword } } } buildTypes { // Unit test coverage as JaCoCo XML for the hub's Coverage tab // (#19): ./gradlew :app:createDebugUnitTestCoverageReport. debug { enableUnitTestCoverage = true } release { optimization { enable = false } signingConfigs.findByName("release")?.let { signingConfig = it } } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } packaging { jniLibs { useLegacyPackaging = true } } buildFeatures { compose = true buildConfig = true } // README screenshots, rendered from previews without an emulator: // ./gradlew :app:updateDebugScreenshotTest (src/screenshotTest). experimentalProperties["android.experimental.enableScreenshotTest"] = true sourceSets["main"].jniLibs.directories.add(rustJniLibs.get().asFile.path) sourceSets["main"].kotlin.directories.add(rustBindings.get().asFile.path) } dependencies { implementation(libs.jna) { artifact { type = "aar" } } implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.messaging) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3.navigation.suite) implementation(libs.androidx.compose.material3.adaptive) implementation(libs.androidx.compose.material3.adaptive.layout) implementation(libs.androidx.compose.material3.adaptive.navigation) implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.glance.appwidget) implementation(libs.androidx.glance.material3) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.navigation.compose) implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.coroutines.android) implementation(libs.okhttp) implementation(libs.markdown.renderer.m3) implementation(libs.highlights) testImplementation(libs.junit) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.okhttp.mockwebserver) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.junit) screenshotTestImplementation(libs.screenshot.validation.api) screenshotTestImplementation(libs.androidx.compose.ui.tooling) screenshotTestImplementation(libs.kotlinx.coroutines.test) debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.tooling) }