* feat(chat): message persistence + background notifications
Two critical gaps for daily-driver use:
1. Message persistence (Room database):
- New ChatMessageEntity / ChatMessageDao / HermesDatabase layer
- Every user message, streaming token, tool result, and assistant
reply is written to a local Room DB as it arrives
- switchSession loads cached messages instantly from Room before
the REST call returns — no more blank screen while loading
- Session resume also loads from cache first
- Messages survive app kills, process death, and configuration
changes
- ChatViewModel changed from ViewModel() to AndroidViewModel(app)
to access the database context
2. Background notifications (foreground service):
- ChatNotificationService keeps the WebSocket alive while the app
is backgrounded and posts a high-priority notification when a
MessageComplete or ClarifyRequest event arrives while the app
is not in the foreground
- Lifecycle wired via LifecycleEventObserver in ChatScreen:
ON_STOP starts the service, ON_START stops it
- Tapping the notification returns to the chat
- New permissions: POST_NOTIFICATIONS, FOREGROUND_SERVICE,
FOREGROUND_SERVICE_DATA_SYNC
Build changes:
- Added Room 2.7.1 + KSP to version catalog
- Added WorkManager 2.10.0 (reserved for future polling fallback)
- Updated ChatViewModelTest to mock HermesDatabase + use
AndroidViewModel constructor
* fix(build): use KSP 2.3.9 (new versioning scheme for Kotlin 2.3.x)
* fix(build): add Room schemaDirectory (required by Room Gradle plugin)
* fix(compile): add else branch to WsEvent when expression (exhaustiveness)
* ci(release): add release workflow + R8 minification + proguard rules
New .github/workflows/release.yml:
- Triggers on tag pushes (v*) and manual workflow_dispatch
- Runs full CI (ktlint + lint + test) before building
- Builds assembleRelease with version injection from tag
- Creates a GitHub Release with auto-generated changelog
- Attaches the release APK to the release
Build changes:
- Release builds now use R8 minification (isMinifyEnabled=true)
and resource shrinking (isShrinkResources=true) for smaller APKs
- versionCode/versionName injectable via -PversionCode / -PversionName
Gradle properties (passed by CI from the git tag)
- Local dev builds default to versionCode=1, versionName=1.0-dev
ProGuard rules:
- Keep all data model classes (Gson reflection)
- Keep Room entities and RoomDatabase subclasses
- Keep OkHttp/Retrofit interface methods
- Suppress warnings for Compose, Security Crypto, Coroutines
* feat(ci): release workflow + proguard rules + test fix
Release workflow (.github/workflows/release.yml):
- Triggers on tag push (v*) or manual workflow_dispatch
- Two jobs: ci (ktlint+lint+test) → build-release
- Builds a minified, shrunk release APK with version from tag
- Creates a GitHub Release with auto-generated release notes
- Attaches the APK as a release asset
Build config:
- versionCode/versionName now injected from CI via -P flags
- Release build enables isMinifyEnabled + isShrinkResources
- proguard-rules.pro: keep rules for Gson models, Room entities,
OkHttp/Retrofit interfaces, Coroutines, Compose, BuildConfig
Test fix:
- ChatViewModelTest: use coEvery for suspend DAO methods instead
of every (compile error — suspend functions need coEvery)
- Added stubs for all suspend DAO methods used by the ViewModel
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
140 lines
4.2 KiB
Kotlin
140 lines
4.2 KiB
Kotlin
import org.gradle.api.tasks.testing.Test
|
|
|
|
plugins {
|
|
alias(libs.plugins.android.application)
|
|
alias(libs.plugins.compose.compiler)
|
|
alias(libs.plugins.kotlin.serialization)
|
|
alias(libs.plugins.room)
|
|
alias(libs.plugins.ksp)
|
|
}
|
|
|
|
android {
|
|
namespace = "com.m57.hermescontrol"
|
|
compileSdk = 36
|
|
defaultConfig {
|
|
applicationId = "com.m57.hermescontrol"
|
|
minSdk = 26
|
|
targetSdk = 36
|
|
// Version overrides passed from CI via -PversionName / -PversionCode
|
|
// Falls back to defaults for local development.
|
|
versionCode = (project.findProperty("versionCode") as? String)?.toIntOrNull() ?: 1
|
|
versionName = (project.findProperty("versionName") as? String) ?: "1.0-dev"
|
|
}
|
|
|
|
buildTypes {
|
|
release {
|
|
isMinifyEnabled = true
|
|
isShrinkResources = true
|
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
signingConfig = signingConfigs.getByName("debug")
|
|
}
|
|
}
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
targetCompatibility = JavaVersion.VERSION_17
|
|
}
|
|
buildFeatures {
|
|
compose = true
|
|
aidl = false
|
|
buildConfig = true
|
|
shaders = false
|
|
}
|
|
|
|
packaging {
|
|
resources {
|
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
|
}
|
|
}
|
|
}
|
|
|
|
kotlin {
|
|
jvmToolchain(17)
|
|
}
|
|
|
|
room {
|
|
schemaDirectory("$projectDir/schemas")
|
|
}
|
|
|
|
dependencies {
|
|
val composeBom = platform(libs.androidx.compose.bom)
|
|
implementation(composeBom)
|
|
androidTestImplementation(composeBom)
|
|
|
|
// Core Android dependencies
|
|
implementation(libs.androidx.core.ktx)
|
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
|
implementation(libs.androidx.activity.compose)
|
|
|
|
// Arch Components
|
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
|
|
|
// Compose
|
|
implementation(libs.androidx.compose.ui)
|
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
|
implementation(libs.androidx.compose.material3)
|
|
implementation(libs.androidx.compose.material.icons.extended)
|
|
// Tooling
|
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
|
// Instrumented tests
|
|
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
|
|
|
// Networking
|
|
implementation(libs.okhttp)
|
|
implementation(libs.okhttp.logging)
|
|
implementation(libs.retrofit)
|
|
implementation(libs.retrofit.converter.gson)
|
|
implementation(libs.gson)
|
|
|
|
// Encrypted storage
|
|
implementation(libs.androidx.security.crypto)
|
|
implementation("androidx.startup:startup-runtime:1.1.1")
|
|
|
|
// Local database (Room) — message persistence
|
|
implementation(libs.androidx.room.runtime)
|
|
implementation(libs.androidx.room.ktx)
|
|
ksp(libs.androidx.room.compiler)
|
|
|
|
// Background notifications (WorkManager)
|
|
implementation(libs.androidx.work.runtime)
|
|
|
|
// Local tests: jUnit, coroutines, Android runner
|
|
testImplementation(libs.junit)
|
|
testImplementation(libs.junit.jupiter.api)
|
|
testRuntimeOnly(libs.junit.jupiter.engine)
|
|
testRuntimeOnly(libs.junit.vintage.engine)
|
|
testRuntimeOnly(libs.junit.platform.launcher)
|
|
testImplementation(libs.okhttp.mockwebserver)
|
|
testImplementation(libs.kotlinx.coroutines.test)
|
|
testImplementation(libs.mockk)
|
|
|
|
// Instrumented tests: jUnit rules and runners
|
|
androidTestImplementation(libs.androidx.test.core)
|
|
androidTestImplementation(libs.androidx.test.ext.junit)
|
|
androidTestImplementation(libs.androidx.test.runner)
|
|
androidTestImplementation(libs.androidx.test.espresso.core)
|
|
|
|
// Navigation
|
|
implementation(libs.androidx.navigation3.ui)
|
|
implementation(libs.androidx.navigation3.runtime)
|
|
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
|
|
}
|
|
|
|
tasks.register<Exec>("ktlintCheck") {
|
|
group = "verification"
|
|
description = "Runs ktlint check."
|
|
workingDir = rootProject.projectDir
|
|
commandLine("sh", "-c", "ktlint \"app/src/**/*.kt\"")
|
|
}
|
|
tasks.register<Exec>("ktlintFormat") {
|
|
group = "formatting"
|
|
description = "Runs ktlint format."
|
|
workingDir = rootProject.projectDir
|
|
commandLine("sh", "-c", "ktlint -F \"app/src/**/*.kt\"")
|
|
}
|
|
|
|
tasks.withType<Test> {
|
|
useJUnitPlatform()
|
|
}
|