heimdall-android/CLAUDE.md

89 lines
4.8 KiB
Markdown
Raw Normal View History

2026-06-17 10:56:45 +07:00
# 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
```bash
./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`.