import java.util.Properties import org.gradle.api.tasks.testing.Test plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.compose) } // Optional release signing. // // Create a `keystore.properties` file in the project ROOT (it is git-ignored; see // keystore.properties.example) with: // // storeFile=/absolute/path/to/upload-keystore.jks // storePassword=******** // keyAlias=upload // keyPassword=******** // // Without that file the release build still runs R8/minification but produces an // unsigned APK, which is fine for local checks but cannot be uploaded to Play. val keystorePropertiesFile = rootProject.file("keystore.properties") val keystoreProperties = Properties().apply { if (keystorePropertiesFile.exists()) { keystorePropertiesFile.inputStream().use { load(it) } } } val hasReleaseSigning = keystoreProperties.getProperty("storeFile") != null if (hasReleaseSigning) { // Fail early and readably instead of letting signReleaseBundle blow up later // with an opaque NullPointerException. val missing = listOf("storePassword", "keyAlias") .filter { keystoreProperties.getProperty(it).isNullOrBlank() } check(missing.isEmpty()) { "keystore.properties is incomplete — missing: ${missing.joinToString()}" } } android { namespace = "de.softwareapp_hb.filemanager" compileSdk = 36 defaultConfig { applicationId = "de.softwareapp_hb.filemanager" minSdk = 26 targetSdk = 36 versionCode = 1 versionName = "1.0.0" vectorDrawables { useSupportLibrary = true } } signingConfigs { if (hasReleaseSigning) { create("release") { storeFile = file(keystoreProperties.getProperty("storeFile")) storePassword = keystoreProperties.getProperty("storePassword") keyAlias = keystoreProperties.getProperty("keyAlias") // Falls back to the store password. That matches reality: PKCS12 // keystores (the default on modern JDKs) cannot have a key password // that differs from the store password, and leaving this null makes // AGP's signReleaseBundle crash with an NPE. keyPassword = keystoreProperties.getProperty("keyPassword") ?: keystoreProperties.getProperty("storePassword") } } } buildTypes { debug { applicationIdSuffix = ".debug" versionNameSuffix = "-debug" // Pseudo-locale (en-XA): every string from resources is rendered accented and // wrapped in brackets, so any text that comes out unchanged is text that never // went through `strings.xml`. Set the device language to English (XA) to see it. isPseudoLocalesEnabled = true } release { if (hasReleaseSigning) { signingConfig = signingConfigs.getByName("release") } isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { compose = true } testOptions { unitTests { // Robolectric needs the merged resources and manifest to stand up the framework. isIncludeAndroidResources = true } } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } lint { // Dependency versions are pinned deliberately — Compose 1.12+/Coil 3.6+ need // compileSdk 37, which AGP 9.1 does not support yet (see the README version // note). These advisories are noise rather than signal here. disable += setOf( "GradleDependency", "AndroidGradlePluginVersion", "NewerVersionAvailable", ) } } kotlin { compilerOptions { // With AGP built-in Kotlin this already defaults to // android.compileOptions.targetCompatibility; set explicitly for clarity. jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } // Robolectric reflects into JDK internals (notably java.io.FileDescriptor), which the module // system blocks from JDK 17 onwards. Without these it fails with "Failed to interact with raw // FileDescriptor internals". tasks.withType().configureEach { jvmArgs( "--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED", "--add-opens=java.base/java.util=ALL-UNNAMED", "--add-opens=java.base/java.text=ALL-UNNAMED", "--add-opens=java.base/java.nio=ALL-UNNAMED", "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED", // Robolectric reaches FileDescriptor internals through jdk.internal.access.SharedSecrets, and // that needs an *export* rather than an *open*: without it the run dies with // "IllegalAccessException ... module java.base does not export jdk.internal.access". "--add-exports=java.base/jdk.internal.access=ALL-UNNAMED", "--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED", "--add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED", ) maxHeapSize = "2g" testLogging { showStandardStreams = true } } dependencies { implementation(libs.androidx.core.ktx) // SAF backend for the "All files access denied" fallback path. implementation(libs.androidx.documentfile) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.activity.compose) 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.compose.material.icons.extended) implementation(libs.kotlinx.coroutines.android) // Image loading for thumbnails and the built-in image viewer. implementation(libs.coil.compose) // Video frames, for the same thumbnails: the platform's own retriever, decoding a frame out of // the clip. It registers itself with Coil through a ServiceLoader entry, so this one line is // the whole integration — no ImageLoader has to be assembled by hand for it to take effect. implementation(libs.coil.video) // ZIP/TAR/GZ archive support. Pure Java — no native code, no NDK. implementation(libs.commons.compress) // Commons Compress's JVM-only dependencies for LZMA/LZMA2: `.xz` and 7z. Optional // upstream, so it has to be declared here or those formats fail at runtime. implementation(libs.xz) debugImplementation(libs.androidx.compose.ui.tooling) testImplementation(libs.junit) // Runs the real Android framework on the JVM, so the data layer is testable without a device. testImplementation(libs.robolectric) // Lets a ViewModel test drive `viewModelScope` on a deterministic dispatcher. testImplementation(libs.kotlinx.coroutines.test) }