// SPDX-License-Identifier: GPL-3.0-or-later import java.io.FileInputStream import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) alias(libs.plugins.hilt) } // Release signing material (PLAN.md §12) is never committed. It is read from, in // order of precedence: a local gitignored `keystore.properties` at the repo root, // then environment variables (how CI injects the decoded keystore + secrets). When // none is present, the release build is simply left unsigned — `assembleDebug` and // the PR gate are unaffected, so contributors without the keystore can still build. val keystorePropsFile = rootProject.file("keystore.properties") val keystoreProps = Properties().apply { if (keystorePropsFile.exists()) FileInputStream(keystorePropsFile).use { load(it) } } fun signingValue(propKey: String, envKey: String): String? = keystoreProps.getProperty(propKey) ?: System.getenv(envKey) val ksStoreFilePath = signingValue("storeFile", "ANDROID_KEYSTORE_FILE") val ksStorePassword = signingValue("storePassword", "ANDROID_KEYSTORE_PASSWORD") val ksKeyAlias = signingValue("keyAlias", "ANDROID_KEY_ALIAS") val ksKeyPassword = signingValue("keyPassword", "ANDROID_KEY_PASSWORD") val hasReleaseSigning = ksStoreFilePath != null && ksStorePassword != null && ksKeyAlias != null && ksKeyPassword != null android { namespace = "com.runicgateway.app" compileSdk = 35 defaultConfig { // Target application id per docs/android/PLAN.md §13 (pending runicgateway.app domain). applicationId = "com.runicgateway.app" minSdk = 29 targetSdk = 35 // These committed defaults are the version source of truth (PLAN.md §10). // release.yml's conventional-commit engine bumps versionName here and commits // it on release; versionCode is derived from it (major*10000+minor*100+patch) // so it stays monotonic. Both remain overridable via -P for local/manual builds. versionCode = (project.findProperty("versionCode") as String?)?.toIntOrNull() ?: 1 versionName = (project.findProperty("versionName") as String?)?.takeIf { it.isNotBlank() } ?: "0.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // Android App Links host (docs/android/APP_LINKS.md). autoVerify needs a // *literal* host at build time, so a single multi-tenant APK cannot verify // open-ended shard domains: App Links are a build-time opt-in. Left empty for // the generic build (custom scheme only); a white-label/first-party build // bakes one host with `-PappLinkHost=play.myshard.com`. // • BuildConfig.APP_LINK_HOST — SsoAuthManager reads it to pick the redirect. // • manifestPlaceholder appLinkHost — substituted into the intent-filter host; // empty falls back to the reserved `.invalid` sentinel so the autoVerify // filter is inert (matches no real link, never verifies). val appLinkHost = (project.findProperty("appLinkHost") as String?)?.trim().orEmpty() buildConfigField("String", "APP_LINK_HOST", "\"$appLinkHost\"") manifestPlaceholders["appLinkHost"] = appLinkHost.ifBlank { "runic-gateway.invalid" } } signingConfigs { if (hasReleaseSigning) { create("release") { storeFile = file(ksStoreFilePath!!) storePassword = ksStorePassword keyAlias = ksKeyAlias keyPassword = ksKeyPassword // Sign with both v1 (JAR) and v2 (APK) schemes for broad compatibility. enableV1Signing = true enableV2Signing = true } } } buildTypes { release { // R8 full-mode minify + resource shrink (§7: no offline cache, so a lean // release APK). Keep rules live in proguard-rules.pro. isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) // Signed only when the keystore material is present (local or CI); an // unsigned APK is produced otherwise. The direct-APK release (§10) runs // through release.yml, which supplies the keystore from a Gitea secret. if (hasReleaseSigning) { signingConfig = signingConfigs.getByName("release") } } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" } buildFeatures { compose = true buildConfig = true } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } } dependencies { // Core / lifecycle / activity implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.activity.compose) // Compose (BOM-managed versions) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material.icons.core) implementation(libs.androidx.navigation.compose) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) // DI implementation(libs.hilt.android) ksp(libs.hilt.compiler) implementation(libs.androidx.hilt.navigation.compose) // Networking (wired for M1+; declared now so the stack resolves) implementation(libs.retrofit) implementation(platform(libs.okhttp.bom)) implementation(libs.okhttp) implementation(libs.okhttp.logging.interceptor) implementation(libs.okhttp.sse) implementation(libs.kotlinx.serialization.json) implementation(libs.retrofit.kotlinx.serialization.converter) // Storage implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.security.crypto) // Web hand-off (Chrome Custom Tabs) for register / invite / reset / SSO (§4.2) implementation(libs.androidx.browser) // Images implementation(libs.coil.compose) // Unit tests testImplementation(libs.junit) testImplementation(libs.kotlinx.coroutines.test) // Instrumented tests androidTestImplementation(libs.androidx.test.ext.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) }