import groovy.json.JsonSlurper plugins { id("com.android.application") } // One version for every package (CLAUDE.md): read from the desktop client's // package.json rather than written a second time here. val packageVersion = (JsonSlurper().parse(rootDir.resolve("../meshbay-client/package.json")) as Map<*, *>)["version"] as String val versionParts = packageVersion.split(".").map { it.toInt() } val releaseSigning = listOf("meshbayReleaseStoreFile", "meshbayReleaseStorePassword", "meshbayReleaseKeyAlias", "meshbayReleaseKeyPassword") android { namespace = "org.meshbay.client" compileSdk = 37 defaultConfig { applicationId = "org.meshbay.client" minSdk = 26 targetSdk = 36 versionName = packageVersion versionCode = versionParts[0] * 10000 + versionParts[1] * 100 + versionParts[2] } // The release key never enters the repository: its path and passwords come // from ~/.gradle/gradle.properties. Without them a release build stops // rather than signing with the debug key (see preReleaseBuild below). if (releaseSigning.all { providers.gradleProperty(it).isPresent }) { signingConfigs.create("release") { storeFile = file(providers.gradleProperty("meshbayReleaseStoreFile").get()) storePassword = providers.gradleProperty("meshbayReleaseStorePassword").get() keyAlias = providers.gradleProperty("meshbayReleaseKeyAlias").get() keyPassword = providers.gradleProperty("meshbayReleaseKeyPassword").get() } } // Two builds of the same application, the same id and key. `full` is the // one distributed directly; `play` is for the Play Store, whose policy // gives READ_SMS to the default SMS application only, so it has no // messages backup: not the permission, and not the code that reads them // (src/full, src/play: Flavor.kt). flavorDimensions += "store" productFlavors { create("full") { dimension = "store"; isDefault = true } create("play") { dimension = "store" } } buildTypes { getByName("release") { isMinifyEnabled = false signingConfig = signingConfigs.findByName("release") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { buildConfig = true } testOptions { unitTests.isReturnDefaultValues = false } } dependencies { implementation("androidx.webkit:webkit:1.17.1") implementation("com.squareup.okhttp3:okhttp:5.5.0") // Ed25519, X25519, HKDF and Argon2id, identical on the JVM and every // Android version: the platform has no Argon2 and its Ed25519 is recent. implementation("org.bouncycastle:bcprov-jdk18on:1.86") // Casting: discovery through MediaRouter, control through the cast sender // SDK and the default media receiver. Needs the vendor's play services at // run time; where they are absent the page is offered no cast at all. implementation("com.google.android.gms:play-services-cast-framework:22.3.1") implementation("androidx.mediarouter:mediarouter:1.8.1") // Notifications are fetched with nothing to install; this is only for a // phone that already has a UnifiedPush distributor (ntfy, …), which then // makes them instant, encrypted to the phone (RFC 8291) — no vendor push // service. Apache-2.0, as is Tink, which it brings for the decryption. implementation("org.unifiedpush.android:connector:3.3.5") testImplementation("junit:junit:4.13.2") // Android's org.json is a stub on the JVM; the unit tests need the real one. testImplementation("org.json:json:20260814") } /** * The interface, copied from its single source at build time (§8.3). * * `meshbay-hub/src/meshbay_hub/static/` is the interface for the web, the * desktop application and this one. The copy lands in build/ and is never * committed, so it cannot fork. The page itself is the desktop application's * `scripts/index.html` — the hub builds its own with a /a// prefix that * would point back at the hub — so an application loading from its package * has one page, not two. */ abstract class SyncUi : DefaultTask() { @get:InputDirectory abstract val staticDir: DirectoryProperty @get:InputFile abstract val indexHtml: RegularFileProperty @get:OutputDirectory abstract val outputDir: DirectoryProperty @TaskAction fun copy() { val ui = outputDir.get().asFile.resolve("ui") outputDir.get().asFile.deleteRecursively() staticDir.get().asFile.copyRecursively(ui) indexHtml.get().asFile.copyTo(ui.resolve("index.html"), overwrite = true) } } val syncUi = tasks.register("syncUi") { staticDir.set(rootDir.resolve("../meshbay-hub/src/meshbay_hub/static")) indexHtml.set(rootDir.resolve("../meshbay-client/scripts/index.html")) outputDir.set(layout.buildDirectory.dir("generated/ui-assets")) } // Checked when a release is built, not when the project is configured, so a // debug build and the unit tests need no key. tasks.configureEach { // One per build: preFullReleaseBuild, prePlayReleaseBuild. if (name.startsWith("pre") && name.endsWith("ReleaseBuild")) doFirst { val missing = releaseSigning.filter { !providers.gradleProperty(it).isPresent } if (missing.isNotEmpty()) throw GradleException( "no release key: set ${missing.joinToString()} in ~/.gradle/gradle.properties") } } androidComponents { onVariants { variant -> variant.sources.assets?.addGeneratedSourceDirectory(syncUi, SyncUi::outputDir) } }