| 1 |
import java.io.File |
| 2 |
import java.util.Properties |
| 3 |
|
| 4 |
plugins { |
| 5 |
alias(libs.plugins.android.application) |
| 6 |
alias(libs.plugins.kotlin.compose) |
| 7 |
alias(libs.plugins.kotlin.serialization) |
| 8 |
alias(libs.plugins.screenshot) |
| 9 |
} |
| 10 |
|
| 11 |
// The Rust core (rust-core/, over oxy-core) is built for the app's ABIs by |
| 12 |
// cargo-ndk and its Kotlin bindings generated by uniffi at build time |
| 13 |
// (oxygenhub-android#1): nothing built is kept in the repository. |
| 14 |
val rustCore: File = rootProject.file("rust-core") |
| 15 |
val rustJniLibs = layout.buildDirectory.dir("rustJniLibs") |
| 16 |
val rustBindings = layout.buildDirectory.dir("generated/uniffi/kotlin") |
| 17 |
|
| 18 |
fun androidNdk(): String { |
| 19 |
System.getenv("ANDROID_NDK_HOME")?.let { return it } |
| 20 |
val props = Properties() |
| 21 |
val local = rootProject.file("local.properties") |
| 22 |
if (local.exists()) local.inputStream().use { input -> props.load(input) } |
| 23 |
val sdk = props.getProperty("sdk.dir") ?: System.getenv("ANDROID_HOME") |
| 24 |
?: error("set sdk.dir in local.properties or ANDROID_HOME") |
| 25 |
val ndks = File(sdk, "ndk").listFiles()?.filter { f -> f.isDirectory }?.sortedBy { f -> f.name }.orEmpty() |
| 26 |
return ndks.lastOrNull()?.path ?: error("no NDK under $sdk/ndk: install one with the SDK manager") |
| 27 |
} |
| 28 |
|
| 29 |
val buildRustCore = tasks.register("buildRustCore", Exec::class) { |
| 30 |
description = "Builds rust-core for arm64-v8a and x86_64 with cargo-ndk." |
| 31 |
workingDir = rustCore |
| 32 |
inputs.dir(rustCore.resolve("src")) |
| 33 |
inputs.files(rustCore.resolve("Cargo.toml"), rustCore.resolve("Cargo.lock")) |
| 34 |
// The runner's Rust (OxygenFlow and its oxy-core) is built in too. |
| 35 |
inputs.files(fileTree(rootProject.file("oxygenflow")) { include("**/*.rs", "**/Cargo.toml"); exclude("**/target/**") }) |
| 36 |
outputs.dir(rustJniLibs) |
| 37 |
environment("ANDROID_NDK_HOME", androidNdk()) |
| 38 |
commandLine("cargo", "ndk", "-t", "arm64-v8a", "-t", "x86_64", "--platform", "31", "-o", rustJniLibs.get().asFile.path, "build", "--release") |
| 39 |
} |
| 40 |
|
| 41 |
val generateRustBindings = tasks.register("generateRustBindings", Exec::class) { |
| 42 |
description = "Generates rust-core's Kotlin bindings with uniffi." |
| 43 |
dependsOn(buildRustCore) |
| 44 |
workingDir = rustCore |
| 45 |
inputs.dir(rustJniLibs) |
| 46 |
inputs.file(rustCore.resolve("uniffi.toml")) |
| 47 |
outputs.dir(rustBindings) |
| 48 |
commandLine( |
| 49 |
"cargo", "run", "--release", "--bin", "uniffi-bindgen", "generate", |
| 50 |
"--library", rustJniLibs.get().file("arm64-v8a/librust_core.so").asFile.path, |
| 51 |
"--language", "kotlin", "--out-dir", rustBindings.get().asFile.path, |
| 52 |
) |
| 53 |
} |
| 54 |
|
| 55 |
tasks.named("preBuild") { dependsOn(generateRustBindings) } |
| 56 |
|
| 57 |
// The Firebase client settings alerts need (#16), read from |
| 58 |
// app/google-services.json without Google's Gradle plugin: none, no alerts. |
| 59 |
val firebase: Map<String, String> = run { |
| 60 |
val file = File(projectDir, "google-services.json") |
| 61 |
if (!file.isFile) return@run emptyMap() |
| 62 |
@Suppress("UNCHECKED_CAST") |
| 63 |
val json = groovy.json.JsonSlurper().parse(file) as Map<String, Any?> |
| 64 |
val project = json["project_info"] as Map<String, Any?> |
| 65 |
val client = (json["client"] as List<Map<String, Any?>>).first { |
| 66 |
((it["client_info"] as Map<String, Any?>)["android_client_info"] as Map<String, Any?>)["package_name"] == "io.oxygenhub.android" |
| 67 |
} |
| 68 |
mapOf( |
| 69 |
"app_id" to (client["client_info"] as Map<String, Any?>)["mobilesdk_app_id"].toString(), |
| 70 |
"project_id" to project["project_id"].toString(), |
| 71 |
"sender_id" to project["project_number"].toString(), |
| 72 |
"api_key" to ((client["api_key"] as List<Map<String, Any?>>).first()["current_key"]).toString(), |
| 73 |
) |
| 74 |
} |
| 75 |
|
| 76 |
// The version, from gradle.properties (#7). |
| 77 |
val appVersion: String = providers.gradleProperty("oxygenhub.version").get() |
| 78 |
val appVersionCode: Int = appVersion.split(".").map { it.toInt() }.let { (a, b, c) -> a * 10000 + b * 100 + c } |
| 79 |
|
| 80 |
// Release signing (#7): the release job decodes its keystore secret to a |
| 81 |
// file and names it and the password's file here. Without them a release |
| 82 |
// build is unsigned, so nothing else needs the key. |
| 83 |
val releaseKeystore: File? = providers.environmentVariable("OXYGENHUB_KEYSTORE").orNull?.let { File(it) }?.takeIf { it.isFile } |
| 84 |
val releasePassword: String? = |
| 85 |
providers.environmentVariable("OXYGENHUB_KEYSTORE_PASSWORD_FILE").orNull?.let { File(it).readText().trim() } |
| 86 |
|
| 87 |
android { |
| 88 |
namespace = "io.oxygenhub.android" |
| 89 |
compileSdk { |
| 90 |
version = release(37) |
| 91 |
} |
| 92 |
|
| 93 |
defaultConfig { |
| 94 |
applicationId = "io.oxygenhub.android" |
| 95 |
minSdk = 31 |
| 96 |
targetSdk = 37 |
| 97 |
versionCode = appVersionCode |
| 98 |
versionName = appVersion |
| 99 |
|
| 100 |
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" |
| 101 |
// Firebase for alerts (#16), from google-services.json when the |
| 102 |
// build has one (the release job writes it from a secret); empty, |
| 103 |
// the app takes no alerts. |
| 104 |
buildConfigField("String", "FIREBASE_APP_ID", "\"${firebase["app_id"].orEmpty()}\"") |
| 105 |
buildConfigField("String", "FIREBASE_PROJECT_ID", "\"${firebase["project_id"].orEmpty()}\"") |
| 106 |
buildConfigField("String", "FIREBASE_SENDER_ID", "\"${firebase["sender_id"].orEmpty()}\"") |
| 107 |
buildConfigField("String", "FIREBASE_API_KEY", "\"${firebase["api_key"].orEmpty()}\"") |
| 108 |
} |
| 109 |
|
| 110 |
signingConfigs { |
| 111 |
if (releaseKeystore != null && releasePassword != null) { |
| 112 |
create("release") { |
| 113 |
storeFile = releaseKeystore |
| 114 |
storeType = "PKCS12" |
| 115 |
storePassword = releasePassword |
| 116 |
keyAlias = "oxygenhub" |
| 117 |
keyPassword = releasePassword |
| 118 |
} |
| 119 |
} |
| 120 |
} |
| 121 |
|
| 122 |
buildTypes { |
| 123 |
// Unit test coverage as JaCoCo XML for the hub's Coverage tab |
| 124 |
// (#19): ./gradlew :app:createDebugUnitTestCoverageReport. |
| 125 |
debug { |
| 126 |
enableUnitTestCoverage = true |
| 127 |
} |
| 128 |
release { |
| 129 |
optimization { |
| 130 |
enable = false |
| 131 |
} |
| 132 |
signingConfigs.findByName("release")?.let { signingConfig = it } |
| 133 |
} |
| 134 |
} |
| 135 |
compileOptions { |
| 136 |
sourceCompatibility = JavaVersion.VERSION_11 |
| 137 |
targetCompatibility = JavaVersion.VERSION_11 |
| 138 |
} |
| 139 |
packaging { |
| 140 |
jniLibs { |
| 141 |
useLegacyPackaging = true |
| 142 |
} |
| 143 |
} |
| 144 |
buildFeatures { |
| 145 |
compose = true |
| 146 |
buildConfig = true |
| 147 |
} |
| 148 |
// README screenshots, rendered from previews without an emulator: |
| 149 |
// ./gradlew :app:updateDebugScreenshotTest (src/screenshotTest). |
| 150 |
experimentalProperties["android.experimental.enableScreenshotTest"] = true |
| 151 |
sourceSets["main"].jniLibs.directories.add(rustJniLibs.get().asFile.path) |
| 152 |
sourceSets["main"].kotlin.directories.add(rustBindings.get().asFile.path) |
| 153 |
} |
| 154 |
|
| 155 |
dependencies { |
| 156 |
implementation(libs.jna) { artifact { type = "aar" } } |
| 157 |
implementation(platform(libs.androidx.compose.bom)) |
| 158 |
implementation(platform(libs.firebase.bom)) |
| 159 |
implementation(libs.firebase.messaging) |
| 160 |
implementation(libs.androidx.activity.compose) |
| 161 |
implementation(libs.androidx.compose.material3) |
| 162 |
implementation(libs.androidx.compose.material3.navigation.suite) |
| 163 |
implementation(libs.androidx.compose.material3.adaptive) |
| 164 |
implementation(libs.androidx.compose.material3.adaptive.layout) |
| 165 |
implementation(libs.androidx.compose.material3.adaptive.navigation) |
| 166 |
implementation(libs.androidx.compose.material.icons.extended) |
| 167 |
implementation(libs.androidx.compose.ui) |
| 168 |
implementation(libs.androidx.compose.ui.graphics) |
| 169 |
implementation(libs.androidx.compose.ui.tooling.preview) |
| 170 |
implementation(libs.androidx.core.ktx) |
| 171 |
implementation(libs.androidx.lifecycle.runtime.ktx) |
| 172 |
implementation(libs.androidx.lifecycle.runtime.compose) |
| 173 |
implementation(libs.androidx.lifecycle.viewmodel.compose) |
| 174 |
implementation(libs.androidx.navigation.compose) |
| 175 |
implementation(libs.kotlinx.serialization.json) |
| 176 |
implementation(libs.kotlinx.coroutines.android) |
| 177 |
implementation(libs.okhttp) |
| 178 |
implementation(libs.markdown.renderer.m3) |
| 179 |
implementation(libs.highlights) |
| 180 |
testImplementation(libs.junit) |
| 181 |
testImplementation(libs.kotlinx.coroutines.test) |
| 182 |
testImplementation(libs.okhttp.mockwebserver) |
| 183 |
androidTestImplementation(platform(libs.androidx.compose.bom)) |
| 184 |
androidTestImplementation(libs.androidx.compose.ui.test.junit4) |
| 185 |
androidTestImplementation(libs.androidx.espresso.core) |
| 186 |
androidTestImplementation(libs.androidx.junit) |
| 187 |
screenshotTestImplementation(libs.screenshot.validation.api) |
| 188 |
screenshotTestImplementation(libs.androidx.compose.ui.tooling) |
| 189 |
screenshotTestImplementation(libs.kotlinx.coroutines.test) |
| 190 |
debugImplementation(libs.androidx.compose.ui.test.manifest) |
| 191 |
debugImplementation(libs.androidx.compose.ui.tooling) |
| 192 |
} |