plugins { id "com.android.application" id "org.jetbrains.kotlin.android" id "org.jetbrains.kotlin.plugin.compose" id "org.jetbrains.kotlin.plugin.parcelize" id "org.jetbrains.kotlin.plugin.serialization" id "com.google.devtools.ksp" id "com.google.dagger.hilt.android" } dependencyLocking { lockAllConfigurations() } android { namespace = "com.peachpasswords" compileSdk = 35 // VERSION is the single semantic-version authority. versionCode remains // Android/Gradle metadata because Kepr's manifest does not carry it; the // updater enforces exact VERSION plus a strictly newer code. def versionAuthorityFile = rootProject.file("VERSION") if (!versionAuthorityFile.isFile()) { throw new GradleException("VERSION is missing; it is the semantic-version authority") } def semanticVersion = versionAuthorityFile.getText("UTF-8").trim() if (!(semanticVersion ==~ /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/)) { throw new GradleException("VERSION must contain exactly one strict major.minor.patch value") } def versionParts = semanticVersion.split("\\.") def vMajor = versionParts[0].toInteger() def vMinor = versionParts[1].toInteger() def vPatch = versionParts[2].toInteger() if (vMajor > 99 || vMinor > 99 || vPatch > 999) { throw new GradleException("VERSION components exceed versionCode encoding range (major/minor <= 99, patch <= 999)") } // Derive a strictly-monotonic versionCode from semver so it can never // accidentally stay flat across releases (which breaks the updater's // versionCode > currentVersionCode identity gate). def derivedVersionCode = vMajor * 1000000 + vMinor * 10000 + vPatch * 10 defaultConfig { applicationId = "com.peachpasswords" minSdk = 26 targetSdk = 35 versionCode = derivedVersionCode versionName = semanticVersion testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } splits { abi { enable = true reset() include "arm64-v8a", "x86_64" universalApk = false } } signingConfigs { release { // PB-26-001: signing passwords are injected via environment // variables (sourced from ~/.secrets), never stored in // repository-adjacent files. The keystore path and alias are // non-secret and remain in local.properties. def props = new Properties() def localPropertiesFile = rootProject.file("local.properties") if (localPropertiesFile.isFile()) { localPropertiesFile.withInputStream { props.load(it) } } def storeFilePath = props.getProperty("peachStoreFile") def alias = props.getProperty("peachKeyAlias") def storePass = System.getenv("PEACH_STORE_PASSWORD") def keyPass = System.getenv("PEACH_KEY_PASSWORD") if (gradle.startParameter.taskNames.any { it.toLowerCase().contains("release") }) { def missing = [] if (!storeFilePath) missing << "peachStoreFile" if (!storePass) missing << "PEACH_STORE_PASSWORD" if (!alias) missing << "peachKeyAlias" if (!keyPass) missing << "PEACH_KEY_PASSWORD" if (!missing.empty) { throw new GradleException( "Missing release signing inputs: ${missing.join(", ")}. " + "Passwords come from PEACH_STORE_PASSWORD / PEACH_KEY_PASSWORD env vars.", ) } } if (storeFilePath != null) { storeFile file(storeFilePath) storePassword = storePass keyAlias = alias keyPassword = keyPass } enableV2Signing = true enableV3Signing = true } } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-debug" debuggable = true } release { minifyEnabled = true shrinkResources = true proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" signingConfig = signingConfigs.release } } // Kepr Beta APKs use the established certificate so installed users can update. compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" } buildFeatures { compose = true buildConfig = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } packaging { resources { excludes += [ "/META-INF/{AL2.0,LGPL2.1}", "META-INF/DEPENDENCIES", "META-INF/LICENSE*", "META-INF/NOTICE*", ] } } testOptions { unitTests.returnDefaultValues = true unitTests.all { def harnessNonce = System.getenv("PEACH_SYNC_HARNESS_NONCE") if (harnessNonce != null) inputs.property("peachSyncHarnessNonce", harnessNonce) def coreDir = rootProject.file("../peach-browser/crypto-core") def nativeLibrary = new File( coreDir, "target/release/libpeach_noise_core.dylib", ) if (coreDir.exists()) { dependsOn rootProject.tasks.named(name.contains("Test") ? "buildRustNoiseHostTest" : "buildRustNoiseHost") systemProperty "java.library.path", nativeLibrary.parentFile.absolutePath } } } } dependencies { implementation platform("androidx.compose:compose-bom:2024.06.00") androidTestImplementation platform("androidx.compose:compose-bom:2024.06.00") implementation "androidx.activity:activity-compose:1.9.2" implementation "androidx.autofill:autofill:1.1.0" implementation "androidx.biometric:biometric:1.1.0" implementation "androidx.core:core-splashscreen:1.0.1" implementation "androidx.appcompat:appcompat:1.7.0" implementation "androidx.credentials:credentials:1.5.0" implementation "androidx.credentials:credentials-play-services-auth:1.5.0" implementation "androidx.glance:glance-appwidget:1.1.0" implementation "androidx.glance:glance-material3:1.1.0" implementation "androidx.hilt:hilt-navigation-compose:1.2.0" // In-app updates: a periodic release check that survives Doze without a // service. hilt-work lets the worker take UpdateRepository by injection // rather than reaching for a global. implementation "androidx.hilt:hilt-work:1.2.0" ksp "androidx.hilt:hilt-compiler:1.2.0" implementation "androidx.work:work-runtime-ktx:2.9.1" implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.4" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4" implementation "androidx.lifecycle:lifecycle-process:2.8.4" implementation "androidx.navigation:navigation-compose:2.8.0" // Deliberately pinned to 1.1.0-alpha06 (audit M-12). 1.1.0 stable exists // now and GradleDependency lint flags this coordinate; a bump is a policy // choice (Tink runtime vs. baseline AES, master-key rotation semantics), // not an oversight, and is tracked separately. Exact alpha (never a floating // range) so the resolved artifact is reproducible. Used only for the // duress/license EncryptedSharedPreferences; the vault slots are sealed by // VaultCrypto, not this. implementation "androidx.security:security-crypto:1.1.0-alpha06" implementation "androidx.compose.material3:material3" implementation "androidx.compose.ui:ui" implementation "androidx.compose.ui:ui-tooling-preview" implementation "androidx.compose.foundation:foundation" implementation "androidx.compose.material:material-icons-extended" implementation "androidx.core:core-ktx:1.13.1" implementation "com.google.android.material:material:1.12.0" implementation "com.google.dagger:hilt-android:2.51.1" ksp "com.google.dagger:hilt-compiler:2.51.1" implementation "com.google.zxing:core:3.5.3" implementation "androidx.camera:camera-core:1.3.4" implementation "androidx.camera:camera-camera2:1.3.4" implementation "androidx.camera:camera-lifecycle:1.3.4" implementation "androidx.camera:camera-view:1.3.4" implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "org.bouncycastle:bcprov-jdk18on:1.84" // zstd-jni (reference libzstd) — per-section compression for the PeachScript // PCHS binary format (MF-2). Required for cross-platform interop with the // browser's zstd sections. The @aar variant ships the Android jniLibs // (lib//libzstd-jni.so); the plain jar bundles only desktop natives and // would fail with UnsatisfiedLinkError on-device. implementation "com.github.luben:zstd-jni:1.5.6-3@aar" // The @aar ships only Android .so; the plain jar carries the desktop natives // the host JVM unit tests need. testImplementation "com.github.luben:zstd-jni:1.5.6-3" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1" debugImplementation "androidx.compose.ui:ui-tooling" debugImplementation "androidx.compose.ui:ui-test-manifest" androidTestImplementation "androidx.test.ext:junit:1.2.1" androidTestImplementation "androidx.test:core:1.6.1" androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1" androidTestImplementation "androidx.test.uiautomator:uiautomator:2.3.0" androidTestImplementation "androidx.compose.ui:ui-test-junit4" testImplementation "junit:junit:4.13.2" // Real org.json on the unit-test classpath; android.jar's is a throwing stub, // so the release-manifest parser would otherwise be untestable off-device. testImplementation "org.json:json:20240303" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1" } // PB-26-144: the alpha-train Play-Services identity-credentials artifact is // reviewed by decision rather than removed, pending non-Play device evidence. // A silent bump must fail with the review requirement named. The check is // deferred to release-task execution: resolving the release runtime classpath // at configuration time fails the variant model and would slow every build. def pinnedCredentialsIdentityVersion = "16.0.0-alpha02" tasks.matching { it.name in ["assembleRelease", "bundleRelease", "packageRelease"] }.configureEach { doFirst { def resolved = configurations.getByName("releaseRuntimeClasspath") .resolvedConfiguration.resolvedArtifacts.find { it.moduleVersion.id.group == "com.google.android.gms" && it.moduleVersion.id.name == "play-services-identity-credentials" } if (resolved == null || resolved.moduleVersion.id.version != pinnedCredentialsIdentityVersion) { def resolvedVersion = resolved?.moduleVersion?.id?.version ?: "missing" throw new GradleException( "play-services-identity-credentials must remain pinned to ${pinnedCredentialsIdentityVersion}; " + "resolved ${resolvedVersion}. Review the alpha artifact's manifest components and GMS " + "metadata, and update docs/ANDROID-RELEASE-MANIFEST-INVENTORY.md, before bumping it.", ) } } }