589 lines
21 KiB
Kotlin
589 lines
21 KiB
Kotlin
import org.gradle.api.services.BuildService
|
|
import org.gradle.api.services.BuildServiceParameters
|
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
|
|
plugins {
|
|
alias(libs.plugins.androidApplication)
|
|
alias(libs.plugins.googleServices)
|
|
alias(libs.plugins.jetbrainsComposeCompiler)
|
|
alias(libs.plugins.serialization)
|
|
alias(libs.plugins.googleKsp)
|
|
}
|
|
|
|
fun getCurrentBranch(workingDir: java.io.File): String =
|
|
try {
|
|
val process =
|
|
ProcessBuilder("git", "rev-parse", "--abbrev-ref", "HEAD")
|
|
.directory(workingDir)
|
|
.redirectErrorStream(true)
|
|
.start()
|
|
val branch =
|
|
process.inputStream
|
|
.bufferedReader()
|
|
.use { it.readText() }
|
|
.trim()
|
|
val exitCode = process.waitFor()
|
|
if (exitCode != 0) "unknown" else branch
|
|
} catch (e: Exception) {
|
|
println("Could not determine git branch: ${e.message}")
|
|
"unknown"
|
|
}
|
|
|
|
fun generateVersionName(
|
|
baseVersion: String,
|
|
workingDir: java.io.File,
|
|
): String {
|
|
val currentBranch = getCurrentBranch(workingDir)
|
|
|
|
if (currentBranch == "main" || currentBranch == "master" || currentBranch == "unknown" || currentBranch == "HEAD") {
|
|
return baseVersion
|
|
}
|
|
|
|
// Clean branch name for version (replace special characters)
|
|
var cleanBranch = currentBranch.replace(Regex("[^a-zA-Z0-9\\-_]"), "-")
|
|
|
|
// Limit branch name to maximum 20 characters
|
|
if (cleanBranch.length > 20) {
|
|
cleanBranch = cleanBranch.substring(0, 20)
|
|
}
|
|
|
|
return "$baseVersion-$cleanBranch"
|
|
}
|
|
|
|
// Workaround: stability.analyzer plugin doesn't declare task dependencies properly for Gradle 9.x
|
|
afterEvaluate {
|
|
val stabilityNames = tasks.names.filter { it.contains("StabilityCheck") }
|
|
val compileNames = tasks.names.filter { it.matches(Regex("compile.*UnitTestKotlin")) }
|
|
stabilityNames.forEach { scName ->
|
|
compileNames.forEach { ctName ->
|
|
tasks.named(scName).configure { mustRunAfter(tasks.named(ctName)) }
|
|
}
|
|
}
|
|
}
|
|
|
|
android {
|
|
namespace = "com.vitorpamplona.amethyst"
|
|
compileSdk =
|
|
libs.versions.android.compileSdk
|
|
.get()
|
|
.toInt()
|
|
|
|
defaultConfig {
|
|
applicationId = "com.vitorpamplona.amethyst"
|
|
minSdk =
|
|
libs.versions.android.minSdk
|
|
.get()
|
|
.toInt()
|
|
targetSdk =
|
|
libs.versions.android.targetSdk
|
|
.get()
|
|
.toInt()
|
|
versionCode =
|
|
libs.versions.appCode
|
|
.get()
|
|
.toInt()
|
|
versionName = generateVersionName(libs.versions.app.get(), rootDir)
|
|
buildConfigField("String", "RELEASE_NOTES_ID", "\"f54843af6397f78e39fa75dbe3b7f7de14eb18c4f9c56e60e7825a2c6715719b\"")
|
|
|
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
vectorDrawables {
|
|
useSupportLibrary = true
|
|
}
|
|
}
|
|
|
|
androidResources {
|
|
localeFilters +=
|
|
listOf(
|
|
"ar",
|
|
"ar-rSA",
|
|
"bn-rBD",
|
|
"cs",
|
|
"cs-rCZ",
|
|
"cy-rGB",
|
|
"da-rDK",
|
|
"de",
|
|
"de-rDE",
|
|
"el-rGR",
|
|
"en-rGB",
|
|
"eo",
|
|
"eo-rUY",
|
|
"es",
|
|
"es-rES",
|
|
"es-rMX",
|
|
"es-rUS",
|
|
"et-rEE",
|
|
"fa",
|
|
"fa-rIR",
|
|
"fi-rFI",
|
|
"fo-rFO",
|
|
"fr",
|
|
"fr-rCA",
|
|
"fr-rFR",
|
|
"gu-rIN",
|
|
"hi-rIN",
|
|
"hr-rHR",
|
|
"hu",
|
|
"hu-rHU",
|
|
"in",
|
|
"in-rID",
|
|
"it-rIT",
|
|
"iw-rIL",
|
|
"ja",
|
|
"ja-rJP",
|
|
"kk-rKZ",
|
|
"ko-rKR",
|
|
"ks-rIN",
|
|
"ku-rTR",
|
|
"lt-rLT",
|
|
"ne-rNP",
|
|
"nl",
|
|
"nl-rBE",
|
|
"nl-rNL",
|
|
"pcm-rNG",
|
|
"pl-rPL",
|
|
"pt-rBR",
|
|
"pt-rPT",
|
|
"ru",
|
|
"ru-rRU",
|
|
"ru-rUA",
|
|
"sa-rIN",
|
|
"sl-rSI",
|
|
"so-rSO",
|
|
"sr-rSP",
|
|
"ss-rZA",
|
|
"sv-rSE",
|
|
"sw-rKE",
|
|
"sw-rTZ",
|
|
"ta",
|
|
"ta-rIN",
|
|
"th",
|
|
"th-rTH",
|
|
"tr",
|
|
"tr-rTR",
|
|
"uk",
|
|
"uk-rUA",
|
|
"ur-rIN",
|
|
"uz-rUZ",
|
|
"vi-rVN",
|
|
"zh",
|
|
"zh-rCN",
|
|
"zh-rHK",
|
|
"zh-rSG",
|
|
"zh-rTW",
|
|
)
|
|
}
|
|
|
|
// Opt-in fast-build flags. Default behavior is unchanged.
|
|
//
|
|
// -PdisableAbiSplits=true skip per-ABI APK splits; produces a single
|
|
// APK per (flavor, buildType) instead of 5.
|
|
// Cuts ~600 MB of intermediates and several
|
|
// minutes off CI.
|
|
// -PdisableUniversalApk=true when ABI splits are enabled, skip the
|
|
// extra universal APK output. (No effect
|
|
// when disableAbiSplits is also set, since
|
|
// there are no splits to add to.)
|
|
// -Pamethyst.skipMapping=true disable R8 minification on release and
|
|
// benchmark. APK is larger, but builds are
|
|
// much faster and outputs/mapping/ (~260MB)
|
|
// is not produced. Local-dev and PR-CI use
|
|
// only — release pipelines must not set it.
|
|
val disableAbiSplits =
|
|
providers
|
|
.gradleProperty("disableAbiSplits")
|
|
.map { it.toBoolean() }
|
|
.getOrElse(false)
|
|
val disableUniversalApk =
|
|
providers
|
|
.gradleProperty("disableUniversalApk")
|
|
.map { it.toBoolean() }
|
|
.getOrElse(false)
|
|
val skipMapping =
|
|
providers
|
|
.gradleProperty("amethyst.skipMapping")
|
|
.map { it.toBoolean() }
|
|
.getOrElse(false)
|
|
|
|
buildTypes {
|
|
getByName("release") {
|
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
isMinifyEnabled = !skipMapping
|
|
}
|
|
getByName("debug") {
|
|
applicationIdSuffix = ".debug"
|
|
versionNameSuffix = "-DEBUG"
|
|
resValue("string", "app_name", "@string/app_name_debug")
|
|
}
|
|
create("benchmark") {
|
|
initWith(getByName("release"))
|
|
applicationIdSuffix = ".benchmark"
|
|
versionNameSuffix = "-BENCHMARK"
|
|
resValue("string", "app_name", "@string/app_name_benchmark")
|
|
isProfileable = true
|
|
signingConfig = signingConfigs.getByName("debug")
|
|
}
|
|
}
|
|
|
|
// TODO: remove this when lightcompressor uses one MP4 parser only
|
|
packaging {
|
|
resources {
|
|
pickFirsts.add("builddef.lst")
|
|
pickFirsts.add("META-INF/LICENSE.md")
|
|
pickFirsts.add("META-INF/LICENSE-notice.md")
|
|
}
|
|
}
|
|
|
|
flavorDimensions += "channel"
|
|
|
|
productFlavors {
|
|
create("play") {
|
|
isDefault = true
|
|
dimension = "channel"
|
|
buildConfigField("boolean", "IS_CASTING_AVAILABLE", "true")
|
|
}
|
|
|
|
create("fdroid") {
|
|
dimension = "channel"
|
|
buildConfigField("boolean", "IS_CASTING_AVAILABLE", "false")
|
|
}
|
|
}
|
|
|
|
splits {
|
|
abi {
|
|
isEnable = !disableAbiSplits
|
|
reset()
|
|
include("x86", "x86_64", "arm64-v8a", "armeabi-v7a")
|
|
isUniversalApk = !disableUniversalApk
|
|
}
|
|
}
|
|
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_21
|
|
targetCompatibility = JavaVersion.VERSION_21
|
|
}
|
|
|
|
buildFeatures {
|
|
compose = true
|
|
buildConfig = true
|
|
resValues = true
|
|
}
|
|
|
|
// Reproducible builds: keep AGP from embedding the dependency-metadata blob
|
|
// in the APK/AAB. That blob is a protobuf of the resolved dependency tree
|
|
// encrypted with a Google public key; the ciphertext is non-deterministic,
|
|
// so its presence makes every release artifact impossible to reproduce
|
|
// bit-for-bit. Dropping it (F-Droid's documented recommendation) lets
|
|
// F-Droid / Zapstore independently rebuild and verify our developer-signed
|
|
// APKs.
|
|
//
|
|
// Play-channel trade-off: with includeInBundle = false the uploaded .aab no
|
|
// longer carries this metadata, so Play Console's app-dependency insights /
|
|
// known-vulnerability SDK alerts go unpopulated. Uploads still succeed; only
|
|
// that advisory feature is lost.
|
|
dependenciesInfo {
|
|
includeInApk = false
|
|
includeInBundle = false
|
|
}
|
|
|
|
packaging {
|
|
resources {
|
|
excludes += listOf("/META-INF/{AL2.0,LGPL2.1}", "**/libscrypt.dylib")
|
|
}
|
|
}
|
|
|
|
lint {
|
|
disable += "MissingTranslation"
|
|
}
|
|
|
|
testOptions {
|
|
unitTests.isReturnDefaultValues = true
|
|
// Lets TorArtiNativeIntegrationTest's System.loadLibrary("arti_android")
|
|
// find the desktop-host build of our Arti JNI shim. The Android .so
|
|
// variants live in src/main/jniLibs/{arm64-v8a,x86_64}/ and are loaded
|
|
// on-device — this Linux x86_64 .so is just for JVM unit-test runs.
|
|
// -Pamethyst.arti.integration=true opts the (slow, network-dependent)
|
|
// tests in; see TorArtiNativeIntegrationTest.kdoc.
|
|
unitTests.all { test ->
|
|
test.systemProperty(
|
|
"java.library.path",
|
|
"$projectDir/src/test/native-libs/x86_64-linux",
|
|
)
|
|
project
|
|
.findProperty("amethyst.arti.integration")
|
|
?.let { test.systemProperty("amethyst.arti.integration", it.toString()) }
|
|
}
|
|
}
|
|
}
|
|
|
|
// androidx.appfunctions-compiler runs in a per-module mode by default,
|
|
// emitting only the dispatcher Kotlin code. The aggregator that builds
|
|
// the `app_functions.xml` asset (which the system reads to discover our
|
|
// @AppFunction methods) is gated behind this KSP argument — without it,
|
|
// the manifest's `android.app.appfunctions` property points at a file
|
|
// that doesn't exist and the System UI logs "Unable to resolve
|
|
// AppFunctionMetadata." Set on the app module only; library modules
|
|
// (commons/quartz) would set it to "false".
|
|
ksp {
|
|
arg("appfunctions:aggregateAppFunctions", "true")
|
|
}
|
|
|
|
// TODO: until google merges and unifiedpush updates https://github.com/tink-crypto/tink-java-apps/pull/5
|
|
configurations.all {
|
|
val tink = "com.google.crypto.tink:tink-android:1.17.0"
|
|
resolutionStrategy {
|
|
force(tink)
|
|
dependencySubstitution {
|
|
substitute(module("com.google.crypto.tink:tink")).using(module(tink))
|
|
}
|
|
}
|
|
}
|
|
|
|
kotlin {
|
|
compilerOptions {
|
|
jvmTarget.set(JvmTarget.JVM_21)
|
|
}
|
|
}
|
|
|
|
// Gradle schedules Kotlin compilations of different variants of this module
|
|
// concurrently (e.g. playDebug + playBenchmark when CI runs unit tests, lint,
|
|
// and assembleBenchmark in one invocation), but they all share a single Kotlin
|
|
// daemon whose heap (kotlin.daemon.jvmargs) cannot fit two full :amethyst
|
|
// codegen passes — CI runs died with "GC overhead limit exceeded" inside the
|
|
// daemon. This no-op shared build service with maxParallelUsages = 1 tells the
|
|
// scheduler to run this module's Kotlin compile tasks one at a time; other
|
|
// projects' tasks (JVM tests, lint analysis, packaging) still run in parallel.
|
|
//
|
|
// CI-only: the OOM needs a cache-cold compile of several variants at once,
|
|
// which local builds (incremental, usually one variant) don't produce.
|
|
abstract class AmethystKotlinCompileLimiter : BuildService<BuildServiceParameters.None>
|
|
|
|
if (System.getenv("CI") != null) {
|
|
val kotlinCompileLimiter =
|
|
gradle.sharedServices.registerIfAbsent("amethystKotlinCompileLimiter", AmethystKotlinCompileLimiter::class) {
|
|
maxParallelUsages.set(1)
|
|
}
|
|
|
|
tasks.withType<KotlinCompile>().configureEach {
|
|
usesService(kotlinCompileLimiter)
|
|
}
|
|
}
|
|
|
|
composeCompiler {
|
|
reportsDestination = layout.buildDirectory.dir("compose_compiler")
|
|
metricsDestination = layout.buildDirectory.dir("compose_compiler")
|
|
}
|
|
|
|
dependencies {
|
|
implementation(platform(libs.androidx.compose.bom))
|
|
|
|
// Compose composition tracing — DEBUG ONLY, profiling aid (not shipped). Makes each
|
|
// recomposition show up as a NAMED slice in Perfetto system traces so we can see which
|
|
// composable recomposes (e.g. during the cold-start feed first-paint). All Apache-2.0.
|
|
// Usage: runtime-enable, then capture a Perfetto trace with the `track_event` data source:
|
|
// adb shell am broadcast -a androidx.tracing.perfetto.action.ENABLE_TRACING \
|
|
// -n com.vitorpamplona.amethyst.debug/androidx.tracing.perfetto.TracingReceiver
|
|
debugImplementation("androidx.compose.runtime:runtime-tracing")
|
|
debugImplementation("androidx.tracing:tracing-perfetto:1.0.0")
|
|
debugImplementation("androidx.tracing:tracing-perfetto-binary:1.0.0")
|
|
|
|
implementation(project(":quartz"))
|
|
implementation(project(":commons"))
|
|
implementation(project(":nestsClient"))
|
|
implementation(project(":nappletHost"))
|
|
// Compose Multiplatform resources runtime, so app-side screens that share a
|
|
// string with a commons renderer can read commons' generated `Res` directly
|
|
// instead of duplicating the key in the Android res tree.
|
|
implementation(libs.jetbrains.compose.components.resources)
|
|
implementation(libs.androidx.core.ktx)
|
|
implementation(libs.androidx.activity.compose)
|
|
|
|
// Hardened WebView host for sandboxed napplet/nsite rendering (origin-restricted message bridge).
|
|
implementation(libs.androidx.webkit)
|
|
|
|
// Client side of the cross-process UI embedding: renders the sandboxed browser surface (hosted in
|
|
// the keyless `:napplet` process) inside a Compose component in the main app.
|
|
implementation(libs.androidx.privacysandbox.ui.core)
|
|
implementation(libs.androidx.privacysandbox.ui.client)
|
|
|
|
implementation(libs.androidx.ui)
|
|
implementation(libs.androidx.ui.graphics)
|
|
implementation(libs.androidx.ui.tooling.preview)
|
|
|
|
// Needs this to open gallery / image upload
|
|
implementation(libs.androidx.fragment.ktx)
|
|
|
|
// Navigation
|
|
implementation(libs.androidx.navigation.compose)
|
|
|
|
// Material 3 Design
|
|
implementation(libs.androidx.material3)
|
|
|
|
// Adaptive Layout / Two Pane
|
|
implementation(libs.androidx.material3.windowSize)
|
|
implementation(libs.accompanist.adaptive)
|
|
|
|
// Lifecycle
|
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
|
|
|
// Zoomable images
|
|
implementation(libs.zoomable)
|
|
|
|
// Biometrics
|
|
implementation(libs.androidx.biometric.ktx)
|
|
|
|
// Background Work
|
|
implementation(libs.androidx.work.runtime.ktx)
|
|
|
|
// Reads workouts from Android Health Connect (Samsung Health, Google Fit, Fitbit, Garmin, …)
|
|
implementation(libs.androidx.health.connect.client)
|
|
|
|
// Websockets API
|
|
implementation(libs.okhttp)
|
|
implementation(libs.okhttpCoroutines)
|
|
|
|
// Encrypted Key Storage
|
|
implementation(libs.androidx.security.crypto.ktx)
|
|
implementation(libs.androidx.datastore.preferences)
|
|
|
|
// view videos
|
|
implementation(libs.androidx.media3.exoplayer)
|
|
implementation(libs.androidx.media3.exoplayer.hls)
|
|
implementation(libs.androidx.media3.ui.compose.material3)
|
|
implementation(libs.androidx.media3.session)
|
|
|
|
// important for proxy / tor
|
|
implementation(libs.androidx.media3.datasource.okhttp)
|
|
|
|
// Load images from the web.
|
|
implementation(libs.coil.compose)
|
|
// view gifs
|
|
implementation(libs.coil.gif)
|
|
// view svgs
|
|
implementation(libs.coil.svg)
|
|
// enables network for coil
|
|
implementation(libs.coil.okhttp)
|
|
// loads thumbnails for media3
|
|
// TODO: Replace this to the FrameExtractor in media 3
|
|
// when FrameExtractor accepts custom data sources.
|
|
implementation(libs.coil.video)
|
|
|
|
// Permission to upload pictures:
|
|
implementation(libs.accompanist.permissions)
|
|
|
|
// For QR generation
|
|
implementation(libs.zxing)
|
|
implementation(libs.zxing.embedded)
|
|
|
|
// OpenStreetMap tiles for road event location maps (kind 1315/1316)
|
|
implementation(libs.osmdroid.android)
|
|
|
|
// Markdown
|
|
// implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0"
|
|
// implementation "com.halilibo.compose-richtext:richtext-ui-material:0.16.0"
|
|
// implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0"
|
|
|
|
// Markdown (With fix for full-image bleeds)
|
|
implementation(libs.markdown.ui)
|
|
implementation(libs.markdown.ui.material3)
|
|
implementation(libs.markdown.commonmark)
|
|
|
|
// Syntax highlighting for the git repository code browser (Apache-2.0)
|
|
implementation(libs.highlights)
|
|
|
|
// LaTeX math rendering ($...$ and $$...$$ inline equations)
|
|
implementation(libs.jlatexmath.android)
|
|
implementation(libs.jlatexmath.font.greek)
|
|
implementation(libs.jlatexmath.font.cyrillic)
|
|
|
|
// Language picker and Theme chooser
|
|
implementation(libs.androidx.appcompat)
|
|
|
|
// Dynamically adjust between phone and tablet UI
|
|
implementation(libs.androidx.window.core.android)
|
|
|
|
// Local model for language identification
|
|
"playImplementation"(libs.google.mlkit.language.id)
|
|
|
|
// Google services model the translate text
|
|
"playImplementation"(libs.google.mlkit.translate)
|
|
|
|
// On-device AI writing assistance (Gemini Nano via AICore)
|
|
"playImplementation"(libs.google.mlkit.genai.proofreading)
|
|
"playImplementation"(libs.google.mlkit.genai.prompt)
|
|
"playImplementation"(libs.google.mlkit.genai.rewriting)
|
|
|
|
// On-device alt-text suggestions: genai image description (preferred, descriptive sentences)
|
|
// with image-labeling as a keyword-join fallback for devices without AICore.
|
|
"playImplementation"(libs.google.mlkit.genai.image.description)
|
|
|
|
// PushNotifications
|
|
"playImplementation"(platform(libs.firebase.bom))
|
|
"playImplementation"(libs.firebase.messaging)
|
|
|
|
// PushNotifications(FDroid)
|
|
"fdroidImplementation"(libs.unifiedpush)
|
|
|
|
// Google Cast SDK — Chromecast support. Play flavor only because the
|
|
// framework hard-depends on Google Play services, which is unavailable
|
|
// on de-Googled / GrapheneOS devices that ship the F-Droid build.
|
|
"playImplementation"(libs.play.services.cast.framework)
|
|
|
|
// androidx.appfunctions — Gemini App Functions adapter. Pre-stable
|
|
// (alpha) as of May 2026 — scoped to the play channel so the F-Droid
|
|
// build stays free of Google AI dependencies. Surface is an
|
|
// AppFunctionService registered in amethyst/src/play/AndroidManifest.xml,
|
|
// generated at compile time by the KSP-driven appfunctions-compiler.
|
|
"playImplementation"(libs.androidx.appfunctions)
|
|
"playImplementation"(libs.androidx.appfunctions.service)
|
|
"kspPlay"(libs.androidx.appfunctions.compiler)
|
|
|
|
// Charts
|
|
implementation(libs.vico.charts.compose)
|
|
implementation(libs.vico.charts.m3)
|
|
|
|
// Waveform visualizer
|
|
implementation(libs.audiowaveform)
|
|
|
|
// Video compression lib
|
|
implementation(libs.abedElazizShe.video.compressor.fork)
|
|
// Image compression lib
|
|
implementation(libs.zelory.image.compressor)
|
|
|
|
// EXIF metadata stripping
|
|
implementation(libs.androidx.exifinterface)
|
|
|
|
// WebRTC for voice/video calls
|
|
implementation(libs.stream.webrtc.android)
|
|
|
|
// Cbor for cashuB format
|
|
implementation(libs.kotlinx.serialization.cbor)
|
|
|
|
// Kotlin serialization for the times where we need the Json tree and performance is not that important.
|
|
implementation(libs.kotlinx.serialization.json)
|
|
|
|
testImplementation(libs.junit)
|
|
testImplementation(libs.mockk)
|
|
testImplementation(libs.kotlinx.coroutines.test)
|
|
testImplementation(libs.secp256k1.kmp.jni.jvm)
|
|
|
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
|
androidTestImplementation(libs.androidx.junit)
|
|
androidTestImplementation(libs.androidx.junit.ktx)
|
|
androidTestImplementation(libs.androidx.espresso.core)
|
|
androidTestImplementation(libs.androidx.ui.test.junit4)
|
|
androidTestImplementation(libs.mockk.android)
|
|
|
|
debugImplementation(platform(libs.androidx.compose.bom))
|
|
debugImplementation(libs.androidx.ui.tooling)
|
|
debugImplementation(libs.androidx.ui.test.manifest)
|
|
|
|
implementation(libs.androidx.camera.core)
|
|
implementation(libs.androidx.camera.camera2)
|
|
implementation(libs.androidx.camera.lifecycle)
|
|
implementation(libs.androidx.camera.view)
|
|
implementation(libs.androidx.camera.extensions)
|
|
}
|