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 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 = 1 versionName = "1.0.0" 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 = (project.findProperty("KADENCE_KEYSTORE") as String?) ?: System.getenv("KADENCE_KEYSTORE") if (storePath != null && file(storePath).isFile) { storeFile = file(storePath) storePassword = (project.findProperty("KADENCE_KEYSTORE_PASSWORD") as String?) ?: System.getenv("KADENCE_KEYSTORE_PASSWORD") keyAlias = (project.findProperty("KADENCE_KEY_ALIAS") as String?) ?: System.getenv("KADENCE_KEY_ALIAS") ?: "kadence" keyPassword = (project.findProperty("KADENCE_KEY_PASSWORD") as String?) ?: System.getenv("KADENCE_KEY_PASSWORD") } } } 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.", ) } }