ba92cf392d
Commons Compress declares its LZMA/LZMA2 support against org.tukaani:xz as an *optional* dependency, so nothing pulled it in transitively and nothing failed at build time. The app nevertheless advertised .xz as extractable, so the first user to open one would have got NoClassDefFoundError: org/tukaani/xz/FilterOptions. XZCompressorInputStream references org/tukaani/xz/XZInputStream directly. The library is now declared, and a round-trip test covers the path that was silently missing. That same library is what 7z needs, and Commons Compress already ships a pure-Java 7z reader, so 7z extraction costs no native code and no further dependency. The docs had assumed the format was off limits and said so in three places, including the customer-facing store copy. 7z is the only format here that cannot arrive through a stream: its header sits at the end of the file, so the reader has to seek. That is now a capability the backend advertises, StorageSource.openSeekable: the all-files backend answers with a real FileChannel, the SAF fallback with null, so folder mode reports needs-a-seekable-file instead of failing obscurely. Entry names go through the same ArchivePath.safeRelativePath zip-slip guard. 7z creation stays deferred.
196 lines
7.1 KiB
Kotlin
196 lines
7.1 KiB
Kotlin
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<Test>().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)
|
|
|
|
// 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)
|
|
}
|