docker / image (push) Canceled after 14s
lint / ruff (push) Successful in 14s
tests / backend (push) Canceled after 16s
tests / desktop (push) Canceled after 3s
tests / web (push) Canceled after 1s
tests / android-core (push) Canceled after 0s
android / apk (push) Successful in 4m16s
202 lines
8.2 KiB
Kotlin
202 lines
8.2 KiB
Kotlin
plugins {
|
|
// AGP 9 has built-in Kotlin support, so `org.jetbrains.kotlin.android` is gone;
|
|
// only the Compose compiler plugin is still applied explicitly.
|
|
alias(libs.plugins.android.application)
|
|
alias(libs.plugins.kotlin.compose)
|
|
alias(libs.plugins.kotlin.serialization)
|
|
}
|
|
|
|
// ---- the Glyph Matrix SDK, which may or may not be here --------------------
|
|
// Nothing's .aar is not on Maven and its licence forbids redistribution, so it
|
|
// cannot be committed. `./fetch-glyph-sdk.sh` downloads it.
|
|
//
|
|
// Rather than failing the build without it, the rear display is chosen at
|
|
// configuration time: exactly one of two source sets is compiled, each providing
|
|
// `newGlyphDisplay()`. With the .aar the real one binds `GlyphMatrixManager`;
|
|
// without it the no-op returns a display that accepts frames and drops them. The
|
|
// rest of the app does not know which it got, and `:core` — where every frame is
|
|
// actually computed — never depends on either.
|
|
val glyphAar = layout.projectDirectory.file("libs/glyph-matrix-sdk-2.0.aar").asFile
|
|
val hasGlyphSdk = glyphAar.isFile
|
|
|
|
/**
|
|
* One property or environment variable, whichever is set.
|
|
*
|
|
* CI passes these as environment variables and a developer puts them in
|
|
* `~/.gradle/gradle.properties`; neither should have to care which the build reads.
|
|
*/
|
|
fun setting(name: String): String? =
|
|
(project.findProperty(name) as String?)?.takeIf { it.isNotBlank() }
|
|
?: System.getenv(name)?.takeIf { it.isNotBlank() }
|
|
|
|
/**
|
|
* Successive builds have to install over each other, so the code has to climb.
|
|
*
|
|
* It was pinned at 1, which meant every CI artifact looked like the same version to
|
|
* the phone. In CI this is the run number; locally it stays 1, because a developer
|
|
* installing over their own debug build does not care.
|
|
*/
|
|
val buildVersionCode = setting("KADENCE_VERSION_CODE")?.toIntOrNull() ?: 1
|
|
val buildVersionName = setting("KADENCE_VERSION_NAME") ?: "0.1.0"
|
|
|
|
android {
|
|
namespace = "tech.kadence.android"
|
|
compileSdk = libs.versions.compileSdk.get().toInt()
|
|
|
|
defaultConfig {
|
|
applicationId = "tech.kadence.android"
|
|
minSdk = libs.versions.minSdk.get().toInt()
|
|
targetSdk = libs.versions.targetSdk.get().toInt()
|
|
versionCode = buildVersionCode
|
|
versionName = buildVersionName
|
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
|
|
buildConfigField("boolean", "HAS_GLYPH_SDK", hasGlyphSdk.toString())
|
|
|
|
// What onboarding pre-fills. Only a starting guess — the field is editable,
|
|
// and `KadencePrefs.normalizeBaseUrl` copes with a LAN address typed instead.
|
|
// Both build types get the real deployment rather than an emulator loopback,
|
|
// because this app's backend is a real host and `10.0.2.2` is only ever right
|
|
// inside an emulator.
|
|
buildConfigField("String", "DEFAULT_BACKEND", "\"https://kadence.timka.su\"")
|
|
}
|
|
|
|
sourceSets {
|
|
getByName("main") {
|
|
kotlin.srcDir(if (hasGlyphSdk) "src/glyph/kotlin" else "src/noglyph/kotlin")
|
|
}
|
|
}
|
|
|
|
signingConfigs {
|
|
// Release signing comes from ~/.gradle/gradle.properties or the environment,
|
|
// never from the repo. Absent them, `assembleRelease` falls back to the debug
|
|
// key: it still produces an installable APK for one's own phone, which is the
|
|
// only distribution this app has.
|
|
create("release") {
|
|
val storePath = setting("KADENCE_KEYSTORE")
|
|
if (storePath != null && file(storePath).isFile) {
|
|
val storePassword = setting("KADENCE_KEYSTORE_PASSWORD")
|
|
?: throw GradleException(
|
|
"KADENCE_KEYSTORE is set but KADENCE_KEYSTORE_PASSWORD is " +
|
|
"missing or blank.",
|
|
)
|
|
val keyPassword = setting("KADENCE_KEY_PASSWORD") ?: storePassword
|
|
val alias = setting("KADENCE_KEY_ALIAS") ?: "kadence"
|
|
|
|
storeFile = file(storePath)
|
|
this.storePassword = storePassword
|
|
keyAlias = alias
|
|
this.keyPassword = keyPassword
|
|
} else if (setting("KADENCE_REQUIRE_SIGNING") == "1") {
|
|
// Without this the fallback below quietly produces a *debug-signed*
|
|
// "release" APK: installable, apparently fine, and impossible to ship
|
|
// or to upgrade a real install with. A release job sets this so a
|
|
// missing or misnamed secret fails the build instead of the release.
|
|
throw GradleException(
|
|
"KADENCE_REQUIRE_SIGNING is set but no keystore was found at " +
|
|
"${storePath ?: "<KADENCE_KEYSTORE unset>"}",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
buildTypes {
|
|
debug {
|
|
// So a debug build can sit beside a release one on the same phone.
|
|
applicationIdSuffix = ".debug"
|
|
}
|
|
release {
|
|
// R8 on, because an always-running foreground service wants the smaller,
|
|
// faster build. The keep rules it needs are in proguard-rules.pro.
|
|
isMinifyEnabled = true
|
|
isShrinkResources = true
|
|
proguardFiles(
|
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
"proguard-rules.pro",
|
|
)
|
|
signingConfig = signingConfigs.getByName("release").takeIf {
|
|
it.storeFile != null
|
|
} ?: signingConfigs.getByName("debug")
|
|
}
|
|
}
|
|
|
|
buildFeatures {
|
|
compose = true
|
|
buildConfig = true
|
|
}
|
|
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_21
|
|
targetCompatibility = JavaVersion.VERSION_21
|
|
}
|
|
|
|
testOptions {
|
|
unitTests.isIncludeAndroidResources = true
|
|
|
|
// `android.util.Log` is one of the stubbed-out android.jar methods that throws
|
|
// "not mocked" on the JVM. Most of these tests are deliberately *not* under
|
|
// Robolectric — `KadenceApiTest` is pure okhttp + MockWebServer and should stay
|
|
// that way — so a single debug log line inside the HTTP client would otherwise
|
|
// fail six unrelated tests. Returning defaults makes Log a no-op there.
|
|
unitTests.isReturnDefaultValues = true
|
|
}
|
|
|
|
packaging {
|
|
resources.excludes += setOf("/META-INF/{AL2.0,LGPL2.1}")
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
// Every piece of logic worth testing lives here, and it has no Android dependency.
|
|
implementation(project(":core"))
|
|
|
|
implementation(libs.serialization.json)
|
|
implementation(libs.okhttp)
|
|
implementation(libs.androidx.core)
|
|
implementation(libs.coroutines.android)
|
|
|
|
implementation(platform(libs.compose.bom))
|
|
implementation(libs.compose.ui)
|
|
implementation(libs.compose.ui.graphics)
|
|
implementation(libs.compose.foundation)
|
|
implementation(libs.compose.material3)
|
|
implementation(libs.compose.ui.tooling.preview)
|
|
debugImplementation(libs.compose.ui.tooling)
|
|
|
|
implementation(libs.activity.compose)
|
|
implementation(libs.navigation.compose)
|
|
implementation(libs.lifecycle.viewmodel.compose)
|
|
implementation(libs.lifecycle.runtime.compose)
|
|
implementation(libs.lifecycle.service)
|
|
|
|
if (hasGlyphSdk) {
|
|
implementation(files(glyphAar))
|
|
}
|
|
|
|
// Robolectric runs these on the JVM, so the UI layer is verifiable without a
|
|
// device or an emulator — the same reason the logic lives in :core.
|
|
testImplementation(libs.junit)
|
|
testImplementation(libs.kotlin.test)
|
|
testImplementation(libs.robolectric)
|
|
testImplementation(libs.coroutines.test)
|
|
testImplementation(libs.mockwebserver)
|
|
testImplementation(platform(libs.compose.bom))
|
|
testImplementation(libs.compose.ui.test.junit4)
|
|
debugImplementation(libs.compose.ui.test.manifest)
|
|
|
|
androidTestImplementation(platform(libs.compose.bom))
|
|
androidTestImplementation(libs.compose.ui.test.junit4)
|
|
debugImplementation(libs.compose.ui.test.manifest)
|
|
}
|
|
|
|
// Make it obvious which rear display got compiled in — otherwise "the Glyph does
|
|
// nothing" is indistinguishable from "the .aar was never downloaded".
|
|
gradle.projectsEvaluated {
|
|
if (!hasGlyphSdk) {
|
|
logger.lifecycle(
|
|
"Glyph Matrix SDK not found at app/libs/ — the rear display is a no-op. " +
|
|
"Run ./fetch-glyph-sdk.sh to enable it.",
|
|
)
|
|
}
|
|
}
|