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() } } 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") 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 { if (name == "preReleaseBuild") 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) } }