heimdall-android/CLAUDE.md
2026-06-17 10:56:45 +07:00

4.8 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project

Heimdall is an Android password manager (Jetpack Compose, Kotlin) that also acts as a system Autofill service. Users register/login against a remote backend, then store and retrieve credentials locally in an encrypted vault.

Build & Test

./gradlew assembleDebug          # build debug APK
./gradlew installDebug           # build + install on connected device/emulator
./gradlew test                   # JVM unit tests (src/test)
./gradlew connectedAndroidTest   # instrumented tests on device (src/androidTest)
./gradlew lint                   # Android lint

# single unit test class / method:
./gradlew test --tests "com.longnh15.heimdall.SomeTest"
./gradlew test --tests "com.longnh15.heimdall.SomeTest.someMethod"
  • Single-module project (:app). Dependencies are managed via the version catalog gradle/libs.versions.toml — add/upgrade libraries there, not inline in build.gradle.kts.
  • minSdk 26, targetSdk/compileSdk 37, Java 11.

Architecture

Clean-architecture-style layering under app/src/main/java/com/longnh15/heimdall/:

  • domain/ — interfaces + models, no Android/framework deps. repository/ holds repo interfaces, model/ holds domain types (ApiResult, AuthInfo, Credential), vault/ holds the session abstraction.
  • data/ — implementations. repository/*Impl implement domain interfaces; source/remote is Retrofit (ApiService + dto/); source/local is Room (HeimdallDatabase, CredentialDao, entity/ + mappers) plus DataStore; preferences/ and vault/ hold other impls.
  • ui/ — Compose screens grouped by feature (auth/login, auth/register, onboarding, vault, vault/edit). Each feature follows MVVM: *Screen (Compose) + *ViewModel + *State + *Event (user intents). State is exposed as StateFlow via stateIn.
  • navigation/Screen is a @Serializable sealed class; routes are type-safe Compose Navigation destinations wired in HeimdallNavGraph.

Key cross-cutting flows:

  • Network: repositories extend BaseRepository and wrap calls in safeApiCall { ... }, which converts a Retrofit Response into ApiResult.Success/ApiResult.Error. Error bodies are parsed by ErrorBodyParser.toApiError(). JSON config lives in common/GlobalConfig.json. Base URL is hardcoded in di/NetworkModule.
  • Auth tokens: on login, the token is encrypted with common/TokenCrypto (AES/GCM key in the Android Keystore, IV prepended) and persisted in the "terces" DataStore via LocalDataSource. Never store the token in plaintext.
  • Vault locking: VaultSessionManager (impl in data/vault) holds an in-memory Locked/Unlocked(expiredAt) StateFlow. HeimdallApplication observes ProcessLifecycleOwner and calls refreshSession() on app foreground, re-locking after the timeout. ViewModels read sessionState to gate access. (Timeout is currently 5 seconds — TIMEOUT_DURATION in VaultSessionManagerImpl.)
  • Autofill: HeimdallAutofillService (registered in the manifest, BIND_AUTOFILL_SERVICE) receives an AssistStructure; StructureParser classifies username/password fields using a layered heuristic (autofillHints → inputType → htmlInfo → text/hint keyword matching). The fill values in buildFillResponse are currently hardcoded placeholders, not yet wired to the vault.
  • First launch / onboarding: MainViewModel reads PreferenceDataSource ("settings" DataStore) to decide the nav start destination (Onboarding vs Login); the splash screen is held until that flag resolves to avoid a flicker.

Dependency Injection (Koin) — note the inconsistency

DI is mid-migration between two Koin styles, and only one is actually loaded:

  • HeimdallApplication.startKoin loads only the manual appModule (di/AppModule.kt), which uses the DSL (single<Impl>() bind Interface::class, viewModel<...>()).
  • Other files use Koin annotations (@Module, @Single in NetworkModule/DatabaseModule, @KoinViewModel, @Singleton) which require the KSP-generated module to be included to take effect. These are not currently passed to startKoin.

When adding a dependency, prefer registering it in appModule so it is actually wired, or explicitly include the annotation-generated modules in startKoin. Don't assume an @Single annotation alone makes a class injectable here.

Conventions

  • Room schemas are exported to app/schemas/; bump the DB version and commit the new schema JSON when changing entities.
  • DTOs (data/source/remote/dto) use kotlinx.serialization (@Serializable); domain models are separate types — map between them in the repository.
  • Validation lives in common/ValidationHelper.