OXYGENHUB
# af9f2e514c72 oxygenhub-android / app / build.gradle.kts
8.3 KiB · last changed Alerts from the hub: the phone registers its Cloud Messaging token and an X25519 key kept by the Keystore, and opens and shows what the hub seals to it with the Rust core; Firebase from google-services.json (the release job's firebase-config secret); pins OxygenFlow main (refs #16) 9 days ago source blame history raw
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
    outputs.dir(rustJniLibs)
35
    environment("ANDROID_NDK_HOME", androidNdk())
36
    commandLine("cargo", "ndk", "-t", "arm64-v8a", "-t", "x86_64", "--platform", "31", "-o", rustJniLibs.get().asFile.path, "build", "--release")
37
}
38
39
val generateRustBindings = tasks.register("generateRustBindings", Exec::class) {
40
    description = "Generates rust-core's Kotlin bindings with uniffi."
41
    dependsOn(buildRustCore)
42
    workingDir = rustCore
43
    inputs.dir(rustJniLibs)
44
    outputs.dir(rustBindings)
45
    commandLine(
46
        "cargo", "run", "--release", "--bin", "uniffi-bindgen", "generate",
47
        "--library", rustJniLibs.get().file("arm64-v8a/librust_core.so").asFile.path,
48
        "--language", "kotlin", "--out-dir", rustBindings.get().asFile.path,
49
    )
50
}
51
52
tasks.named("preBuild") { dependsOn(generateRustBindings) }
53
54
// The Firebase client settings alerts need (#16), read from
55
// app/google-services.json without Google's Gradle plugin: none, no alerts.
56
val firebase: Map<String, String> = run {
57
    val file = File(projectDir, "google-services.json")
58
    if (!file.isFile) return@run emptyMap()
59
    @Suppress("UNCHECKED_CAST")
60
    val json = groovy.json.JsonSlurper().parse(file) as Map<String, Any?>
61
    val project = json["project_info"] as Map<String, Any?>
62
    val client = (json["client"] as List<Map<String, Any?>>).first {
63
        ((it["client_info"] as Map<String, Any?>)["android_client_info"] as Map<String, Any?>)["package_name"] == "io.oxygenhub.android"
64
    }
65
    mapOf(
66
        "app_id" to (client["client_info"] as Map<String, Any?>)["mobilesdk_app_id"].toString(),
67
        "project_id" to project["project_id"].toString(),
68
        "sender_id" to project["project_number"].toString(),
69
        "api_key" to ((client["api_key"] as List<Map<String, Any?>>).first()["current_key"]).toString(),
70
    )
71
}
72
73
// The version, from gradle.properties (#7).
74
val appVersion: String = providers.gradleProperty("oxygenhub.version").get()
75
val appVersionCode: Int = appVersion.split(".").map { it.toInt() }.let { (a, b, c) -> a * 10000 + b * 100 + c }
76
77
// Release signing (#7): the release job decodes its keystore secret to a
78
// file and names it and the password's file here. Without them a release
79
// build is unsigned, so nothing else needs the key.
80
val releaseKeystore: File? = providers.environmentVariable("OXYGENHUB_KEYSTORE").orNull?.let { File(it) }?.takeIf { it.isFile }
81
val releasePassword: String? =
82
    providers.environmentVariable("OXYGENHUB_KEYSTORE_PASSWORD_FILE").orNull?.let { File(it).readText().trim() }
83
84
android {
85
    namespace = "io.oxygenhub.android"
86
    compileSdk {
87
        version = release(37)
88
    }
89
90
    defaultConfig {
91
        applicationId = "io.oxygenhub.android"
92
        minSdk = 31
93
        targetSdk = 37
94
        versionCode = appVersionCode
95
        versionName = appVersion
96
97
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
98
        // Firebase for alerts (#16), from google-services.json when the
99
        // build has one (the release job writes it from a secret); empty,
100
        // the app takes no alerts.
101
        buildConfigField("String", "FIREBASE_APP_ID", "\"${firebase["app_id"].orEmpty()}\"")
102
        buildConfigField("String", "FIREBASE_PROJECT_ID", "\"${firebase["project_id"].orEmpty()}\"")
103
        buildConfigField("String", "FIREBASE_SENDER_ID", "\"${firebase["sender_id"].orEmpty()}\"")
104
        buildConfigField("String", "FIREBASE_API_KEY", "\"${firebase["api_key"].orEmpty()}\"")
105
    }
106
107
    signingConfigs {
108
        if (releaseKeystore != null && releasePassword != null) {
109
            create("release") {
110
                storeFile = releaseKeystore
111
                storeType = "PKCS12"
112
                storePassword = releasePassword
113
                keyAlias = "oxygenhub"
114
                keyPassword = releasePassword
115
            }
116
        }
117
    }
118
119
    buildTypes {
120
        // Unit test coverage as JaCoCo XML for the hub's Coverage tab
121
        // (#19): ./gradlew :app:createDebugUnitTestCoverageReport.
122
        debug {
123
            enableUnitTestCoverage = true
124
        }
125
        release {
126
            optimization {
127
                enable = false
128
            }
129
            signingConfigs.findByName("release")?.let { signingConfig = it }
130
        }
131
    }
132
    compileOptions {
133
        sourceCompatibility = JavaVersion.VERSION_11
134
        targetCompatibility = JavaVersion.VERSION_11
135
    }
136
    packaging {
137
        jniLibs {
138
            useLegacyPackaging = true
139
        }
140
    }
141
    buildFeatures {
142
        compose = true
143
        buildConfig = true
144
    }
145
    // README screenshots, rendered from previews without an emulator:
146
    // ./gradlew :app:updateDebugScreenshotTest (src/screenshotTest).
147
    experimentalProperties["android.experimental.enableScreenshotTest"] = true
148
    sourceSets["main"].jniLibs.directories.add(rustJniLibs.get().asFile.path)
149
    sourceSets["main"].kotlin.directories.add(rustBindings.get().asFile.path)
150
}
151
152
dependencies {
153
    implementation(libs.jna) { artifact { type = "aar" } }
154
    implementation(platform(libs.androidx.compose.bom))
155
    implementation(platform(libs.firebase.bom))
156
    implementation(libs.firebase.messaging)
157
    implementation(libs.androidx.activity.compose)
158
    implementation(libs.androidx.compose.material3)
159
    implementation(libs.androidx.compose.material3.navigation.suite)
160
    implementation(libs.androidx.compose.material3.adaptive)
161
    implementation(libs.androidx.compose.material3.adaptive.layout)
162
    implementation(libs.androidx.compose.material3.adaptive.navigation)
163
    implementation(libs.androidx.compose.material.icons.extended)
164
    implementation(libs.androidx.compose.ui)
165
    implementation(libs.androidx.compose.ui.graphics)
166
    implementation(libs.androidx.compose.ui.tooling.preview)
167
    implementation(libs.androidx.core.ktx)
168
    implementation(libs.androidx.lifecycle.runtime.ktx)
169
    implementation(libs.androidx.lifecycle.runtime.compose)
170
    implementation(libs.androidx.lifecycle.viewmodel.compose)
171
    implementation(libs.androidx.navigation.compose)
172
    implementation(libs.kotlinx.serialization.json)
173
    implementation(libs.kotlinx.coroutines.android)
174
    implementation(libs.okhttp)
175
    implementation(libs.markdown.renderer.m3)
176
    implementation(libs.highlights)
177
    testImplementation(libs.junit)
178
    testImplementation(libs.kotlinx.coroutines.test)
179
    testImplementation(libs.okhttp.mockwebserver)
180
    androidTestImplementation(platform(libs.androidx.compose.bom))
181
    androidTestImplementation(libs.androidx.compose.ui.test.junit4)
182
    androidTestImplementation(libs.androidx.espresso.core)
183
    androidTestImplementation(libs.androidx.junit)
184
    screenshotTestImplementation(libs.screenshot.validation.api)
185
    screenshotTestImplementation(libs.androidx.compose.ui.tooling)
186
    screenshotTestImplementation(libs.kotlinx.coroutines.test)
187
    debugImplementation(libs.androidx.compose.ui.test.manifest)
188
    debugImplementation(libs.androidx.compose.ui.tooling)
189
}