ApplicationId changed from com.roundingmobile.sshworkbench to com.roundingmobile.sshwb (Firebase auto-key blocked Play Console registration). Namespace stays com.roundingmobile.sshworkbench. Web platform in www/: Docker stack (nginx+Node.js+MariaDB), landing page, login (OAuth+email/pw), dashboard (vault sync+session logs), API routes, MariaDB schema. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
259 lines
8 KiB
Kotlin
259 lines
8 KiB
Kotlin
import java.text.SimpleDateFormat
|
|
import java.util.Date
|
|
import java.util.Properties
|
|
|
|
plugins {
|
|
alias(libs.plugins.android.application)
|
|
alias(libs.plugins.kotlin.compose)
|
|
alias(libs.plugins.ksp)
|
|
alias(libs.plugins.hilt.android)
|
|
alias(libs.plugins.google.services)
|
|
alias(libs.plugins.firebase.crashlytics.plugin)
|
|
}
|
|
|
|
val keystoreProps = Properties().apply {
|
|
load(rootProject.file("../.keystore/rmt.properties").inputStream())
|
|
}
|
|
|
|
ksp {
|
|
arg("room.schemaLocation", "$projectDir/schemas")
|
|
}
|
|
|
|
android {
|
|
namespace = "com.roundingmobile.sshworkbench"
|
|
compileSdk {
|
|
version = release(36) {
|
|
minorApiLevel = 1
|
|
}
|
|
}
|
|
|
|
defaultConfig {
|
|
minSdk = 27
|
|
targetSdk = 36
|
|
versionCode = 39
|
|
versionName = "0.0.39"
|
|
|
|
applicationId = "com.roundingmobile.sshwb"
|
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
|
|
ndk {
|
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
|
|
}
|
|
|
|
externalNativeBuild {
|
|
cmake {
|
|
cFlags("-Wall")
|
|
}
|
|
}
|
|
}
|
|
|
|
externalNativeBuild {
|
|
cmake {
|
|
path = file("src/main/cpp/CMakeLists.txt")
|
|
version = "3.22.1"
|
|
}
|
|
}
|
|
|
|
buildFeatures {
|
|
buildConfig = true
|
|
compose = true
|
|
}
|
|
|
|
flavorDimensions += "environment"
|
|
productFlavors {
|
|
create("dev") {
|
|
dimension = "environment"
|
|
applicationIdSuffix = ".dev"
|
|
}
|
|
create("prod") {
|
|
dimension = "environment"
|
|
}
|
|
}
|
|
|
|
signingConfigs {
|
|
create("release") {
|
|
storeFile = rootProject.file("../.keystore/${keystoreProps["keystore"]}")
|
|
storePassword = keystoreProps["keystore.password"] as String
|
|
keyAlias = keystoreProps["release.alias"] as String
|
|
keyPassword = keystoreProps["alias.password"] as String
|
|
}
|
|
}
|
|
|
|
buildTypes {
|
|
debug {
|
|
signingConfig = signingConfigs.getByName("release")
|
|
}
|
|
release {
|
|
isMinifyEnabled = true
|
|
isShrinkResources = true
|
|
signingConfig = signingConfigs.getByName("release")
|
|
proguardFiles(
|
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
"proguard-rules.pro"
|
|
)
|
|
}
|
|
}
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
targetCompatibility = JavaVersion.VERSION_17
|
|
}
|
|
|
|
packaging {
|
|
resources {
|
|
excludes += "META-INF/LICENSE*"
|
|
excludes += "META-INF/NOTICE*"
|
|
excludes += "META-INF/DEPENDENCIES"
|
|
}
|
|
}
|
|
|
|
sourceSets {
|
|
getByName("androidTest").assets.srcDirs("$projectDir/schemas")
|
|
}
|
|
|
|
}
|
|
|
|
// ssh-workbench.v<version>.<free|pro>.<dbg|rel>.YYYY-MM-DD.apk
|
|
// AGP appends "-<flavor>-<buildType>" automatically, so we encode the rest in archivesName
|
|
// and use a finalize task to rename to the exact pattern.
|
|
// Date is set to a placeholder here; the actual date is resolved at execution time in CopyApks.
|
|
val ver: String = android.defaultConfig.versionName ?: "0"
|
|
|
|
base {
|
|
archivesName = "ssh-workbench.v${ver}"
|
|
}
|
|
|
|
abstract class CopyApks : DefaultTask() {
|
|
@get:org.gradle.api.tasks.InputDirectory
|
|
@get:org.gradle.api.tasks.PathSensitive(org.gradle.api.tasks.PathSensitivity.RELATIVE)
|
|
abstract val apkDir: org.gradle.api.file.DirectoryProperty
|
|
|
|
@get:org.gradle.api.tasks.OutputDirectory
|
|
abstract val outputDir: org.gradle.api.file.DirectoryProperty
|
|
|
|
@org.gradle.api.tasks.TaskAction
|
|
fun copyRenamed() {
|
|
val outDir = outputDir.asFile.get()
|
|
outDir.listFiles()?.forEach { it.delete() }
|
|
// Date resolved at execution time — never cached
|
|
val date = SimpleDateFormat("yyyy-MM-dd").format(Date())
|
|
val pattern = Regex("""ssh-workbench\.v(.+?)-(\w+)-(debug|release)\.apk""")
|
|
// Only copy APKs built in the last 2 minutes (skip stale cached APKs)
|
|
val recentThreshold = System.currentTimeMillis() - 120_000
|
|
apkDir.asFile.get().walkTopDown().filter { it.extension == "apk" }.forEach { apk ->
|
|
if (apk.lastModified() < recentThreshold) return@forEach
|
|
val match = pattern.matchEntire(apk.name) ?: return@forEach
|
|
val (v, flavor, buildType) = match.destructured
|
|
val suffix = if (buildType == "debug") "dbg" else "rel"
|
|
val newName = "ssh-workbench.v${v}.${flavor}.${suffix}.${date}.apk"
|
|
apk.copyTo(outDir.resolve(newName), overwrite = true)
|
|
println(" $newName")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write BuildTimestamp.kt into source tree — overwritten each build with current time
|
|
tasks.register("generateBuildTimestamp") {
|
|
val targetFile = layout.projectDirectory.file("src/main/java/com/roundingmobile/sshworkbench/BuildTimestamp.kt")
|
|
outputs.file(targetFile)
|
|
outputs.upToDateWhen { false } // always run
|
|
doLast {
|
|
val time = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date())
|
|
targetFile.asFile.writeText(
|
|
"package com.roundingmobile.sshworkbench\n\n// Auto-generated — do not edit\nobject BuildTimestamp {\n const val TIME = \"$time\"\n}\n"
|
|
)
|
|
}
|
|
}
|
|
tasks.matching {
|
|
(it.name.startsWith("compile") && it.name.endsWith("Kotlin")) ||
|
|
(it.name.startsWith("ksp") && it.name.endsWith("Kotlin"))
|
|
}.configureEach {
|
|
dependsOn("generateBuildTimestamp")
|
|
}
|
|
|
|
tasks.register<CopyApks>("copyApks") {
|
|
description = "Copy APKs with renamed pattern to build/deploy/"
|
|
apkDir = layout.buildDirectory.dir("outputs/apk")
|
|
outputDir = layout.buildDirectory.dir("deploy")
|
|
}
|
|
|
|
tasks.matching { it.name.startsWith("assemble") && it.name != "copyApks" }.configureEach {
|
|
finalizedBy("copyApks")
|
|
}
|
|
|
|
dependencies {
|
|
// Modules
|
|
implementation(project(":lib-terminal-view"))
|
|
implementation(project(":lib-ssh"))
|
|
implementation(project(":lib-terminal-keyboard"))
|
|
implementation(project(":lib-vault-crypto"))
|
|
|
|
// AndroidX Core
|
|
implementation(libs.androidx.core.ktx)
|
|
implementation(libs.androidx.appcompat)
|
|
implementation(libs.material)
|
|
implementation(libs.androidx.activity.ktx)
|
|
|
|
// Lifecycle
|
|
implementation(libs.lifecycle.viewmodel.ktx)
|
|
implementation(libs.lifecycle.runtime.ktx)
|
|
|
|
// Coroutines
|
|
implementation(libs.kotlinx.coroutines.core)
|
|
implementation(libs.kotlinx.coroutines.android)
|
|
|
|
// Room
|
|
implementation(libs.room.runtime)
|
|
implementation(libs.room.ktx)
|
|
ksp(libs.room.compiler)
|
|
|
|
// Security + DataStore
|
|
implementation(libs.security.crypto)
|
|
implementation(libs.datastore.preferences)
|
|
|
|
// Hilt / Dagger
|
|
implementation(libs.hilt.android)
|
|
ksp(libs.hilt.compiler)
|
|
|
|
// Firebase
|
|
implementation(platform(libs.firebase.bom))
|
|
implementation(libs.firebase.analytics)
|
|
implementation(libs.firebase.crashlytics)
|
|
|
|
// Compose
|
|
implementation(platform(libs.compose.bom))
|
|
implementation(libs.compose.ui)
|
|
implementation(libs.compose.material3)
|
|
implementation(libs.compose.foundation)
|
|
implementation(libs.compose.ui.tooling.preview)
|
|
implementation(libs.compose.material.icons)
|
|
implementation(libs.activity.compose)
|
|
implementation(libs.navigation.compose)
|
|
implementation(libs.hilt.navigation.compose)
|
|
implementation(libs.lifecycle.viewmodel.compose)
|
|
implementation(libs.lifecycle.runtime.compose)
|
|
debugImplementation(libs.compose.ui.tooling)
|
|
|
|
// BouncyCastle (for key generation)
|
|
implementation(libs.bouncycastle.prov)
|
|
implementation(libs.bouncycastle.pkix)
|
|
|
|
// Biometric
|
|
implementation(libs.biometric)
|
|
|
|
// ZXing (QR code generation + scanning)
|
|
implementation(libs.zxing.core)
|
|
implementation(libs.zxing.embedded)
|
|
|
|
// Google Play Billing
|
|
implementation(libs.billing.ktx)
|
|
|
|
// ZIP (password-protected archives)
|
|
implementation(libs.zip4j)
|
|
|
|
// Testing
|
|
testImplementation(libs.junit)
|
|
testImplementation(libs.kotlinx.coroutines.test)
|
|
androidTestImplementation(libs.androidx.junit)
|
|
androidTestImplementation(libs.androidx.espresso.core)
|
|
androidTestImplementation(libs.room.testing)
|
|
}
|