Files
LibreMail/app/build.gradle.kts
T
JMR-devandClaude Opus 4.8 8e29aebc2e fix: render reader emails readably in dark mode
The reader rendered HTML emails as black-on-black in dark mode: the WebView
background was transparent (so the near-black app surface showed through) and the
injected CSS set no text or background color, so the WebView fell back to its
default black text.

Wrap each email with explicit, theme-derived background, text, and link colors
(surface / onSurface / primary) plus a matching color-scheme, set the WebView
background to the surface color, and allow WebView algorithmic darkening where
supported as a backstop for emails that hardcode their own foreground colors.

Add JVM contrast guards: HtmlBodyTest pins the wrapper's readability contract
(explicit colors meeting WCAG AA), and ColorSchemeContrastTest audits the
fallback light/dark Material schemes' role pairs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:17:06 -05:00

194 lines
7.8 KiB
Kotlin

// SPDX-License-Identifier: GPL-3.0-or-later
import java.util.Properties
plugins {
// Applied by id; the plugins themselves come from the root buildscript classpath.
// We deliberately do NOT apply org.jetbrains.kotlin.android — AGP's built-in Kotlin
// handles Kotlin compilation (using KGP 2.4.0 from the buildscript classpath).
id("com.android.application")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
// Read the Gmail OAuth client id from secrets.properties (git-ignored). Empty when absent.
val secretsFile = rootProject.file("secrets.properties")
val secrets = Properties().apply {
if (secretsFile.exists()) secretsFile.inputStream().use { load(it) }
}
val gmailOAuthClientId: String = secrets.getProperty("GMAIL_OAUTH_CLIENT_ID", "")
// For a Google installed-app OAuth client, AppAuth's redirect is the reversed client
// id as a custom URI scheme. Fall back to a placeholder so the manifest stays valid
// until a real client id is set in secrets.properties.
val gmailRedirectScheme: String = if (gmailOAuthClientId.endsWith(".apps.googleusercontent.com")) {
"com.googleusercontent.apps." + gmailOAuthClientId.removeSuffix(".apps.googleusercontent.com")
} else {
"org.libremail.oauth"
}
// Microsoft (Outlook) OAuth public client id — a GUID, not a secret. Overridable via
// secrets.properties; defaults to the app's registered client id.
val outlookOAuthClientId: String = secrets.getProperty(
"OUTLOOK_OAUTH_CLIENT_ID",
"04e4aa5e-ed1f-47f9-b567-b99a0b29b3df",
)
// Optional release signing, configured via git-ignored secrets.properties. When absent, release
// builds fall back to the debug key (installable for testing, but not publishable).
val releaseStoreFile: String? = secrets.getProperty("RELEASE_STORE_FILE")
android {
namespace = "org.libremail"
compileSdk = 37
defaultConfig {
applicationId = "org.libremail.app"
// Supports a rolling ~7-year window of Android versions (API 29 / Android 10, 2019 → latest).
minSdk = 29
targetSdk = 37
versionCode = 1
versionName = "0.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "GMAIL_OAUTH_CLIENT_ID", "\"$gmailOAuthClientId\"")
buildConfigField("String", "GMAIL_OAUTH_REDIRECT_URI", "\"$gmailRedirectScheme:/oauth2redirect\"")
buildConfigField("String", "OUTLOOK_OAUTH_CLIENT_ID", "\"$outlookOAuthClientId\"")
buildConfigField("String", "OUTLOOK_OAUTH_REDIRECT_URI", "\"org.libremail.outlook://oauth2redirect\"")
// AppAuth captures the OAuth redirect via this custom scheme.
manifestPlaceholders["appAuthRedirectScheme"] = gmailRedirectScheme
}
signingConfigs {
if (releaseStoreFile != null) {
create("release") {
storeFile = file(releaseStoreFile)
storePassword = secrets.getProperty("RELEASE_STORE_PASSWORD")
keyAlias = secrets.getProperty("RELEASE_KEY_ALIAS")
keyPassword = secrets.getProperty("RELEASE_KEY_PASSWORD")
}
}
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
// Use a dedicated release keystore when configured in secrets.properties; otherwise fall
// back to the debug key so the build is still installable for local testing.
signingConfig = if (releaseStoreFile != null) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
}
}
buildFeatures {
compose = true
buildConfig = true
}
packaging {
resources {
// Angus Mail / Jakarta Activation (added later) ship duplicate META-INF entries.
excludes += setOf(
"/META-INF/{AL2.0,LGPL2.1}",
"/META-INF/DEPENDENCIES",
"/META-INF/LICENSE*",
"/META-INF/NOTICE*",
"/META-INF/INDEX.LIST",
)
}
}
testOptions {
// Gradle Managed Devices define the per-API E2E matrix as config-as-code: one virtual
// device per supported Android version (a rolling ~7-year window, API 29 → latest stable).
// Run the whole matrix with `./gradlew e2eGroupDebugAndroidTest`, or one level with e.g.
// `./gradlew api29DebugAndroidTest`. Gradle provisions/boots/tears down the emulators and
// downloads the system images on first use. Keep this list in lockstep with the CI matrix in
// .github/workflows/ci.yml; when a new Android ships, add it and drop the oldest level that
// has fallen outside ~7 years. API 37 (preview) is exercised on the dev emulator until a
// stable managed-device image is published, so it is intentionally not listed here.
managedDevices {
localDevices {
listOf(29, 30, 31, 32, 33, 34, 35, 36).forEach { api ->
create("api$api") {
device = "Pixel 2"
apiLevel = api
systemImageSource = "google_apis"
}
}
}
groups {
create("e2e") {
targetDevices.addAll(localDevices)
}
}
}
}
}
// Export Room schemas so migrations can be validated by instrumented MigrationTestHelper tests.
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
dependencies {
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)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.webkit)
implementation(libs.kotlinx.coroutines.android)
// Email transport (IMAP/SMTP) + OAuth
implementation(libs.angus.mail)
implementation(libs.appauth)
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)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
compileOnly(libs.error.prone.annotations)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.hilt.work)
ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
implementation(libs.sqlcipher.android)
testImplementation(libs.junit)
testImplementation(libs.kotlin.test)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.turbine)
testImplementation(libs.mockk)
testImplementation(libs.greenmail)
// The real org.json for unit tests (android.jar ships a stubbed, no-op version).
testImplementation("org.json:json:20231013")
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
}