11<!-- FOR AI AGENTS - Scoped to android/ -->
2- <!-- Last updated: 2026-02-25 -->
2+ <!-- Managed by agent: keep sections and order; edit content, not structure -->
3+ <!-- Last updated: 2026-02-25 | Last verified: 2026-02-25 -->
34
45# Android AGENTS.md
56
7+ ## Overview
8+
69Jetpack Compose Android app. Local-first with E2E encrypted sync to Ktor server.
10+ Clean architecture (UI -> Domain -> Data -> Crypto), Hilt DI, Room + SQLCipher, WorkManager sync.
711
8- ## Stack
12+ ** Stack: ** Kotlin, Jetpack Compose, Room + SQLCipher, BouncyCastle + JCA, Hilt, WorkManager, Retrofit, min SDK 26, target SDK 35
913
10- Kotlin, Jetpack Compose, Room + SQLCipher, BouncyCastle + JCA (crypto), Hilt (DI), WorkManager, Retrofit, min SDK 26, target SDK 35
14+ ## Setup
1115
12- ## Package Map
16+ - Android Studio with Kotlin plugin
17+ - JDK 17 for compilation
18+ - No server dependency for unit tests (mocked)
19+
20+ Release signing reads from env vars or ` local.properties ` :
21+ - ` KIDSYNC_KEYSTORE_PATH ` / ` keystore.path `
22+ - ` KIDSYNC_KEYSTORE_PASSWORD ` / ` keystore.password `
23+ - ` KIDSYNC_KEY_ALIAS ` / ` key.alias `
24+ - ` KIDSYNC_KEY_PASSWORD ` / ` key.password `
25+
26+ Only activates when all 4 values present.
27+
28+ ## Commands
29+
30+ | Task | Command | ~ Time |
31+ | ------| ---------| -------|
32+ | Test (all) | ` ./gradlew test ` | ~ 120s |
33+ | Build debug | ` ./gradlew assembleDebug ` | ~ 90s |
34+ | Build release | ` ./gradlew assembleRelease ` | ~ 90s |
35+
36+ ## File Map
1337
1438```
1539app/src/main/java/com/kidsync/app/
1640 KidSyncApplication.kt # @HiltAndroidApp
1741 crypto/
1842 CryptoManager.kt # Interface + buildPayloadAad() helper
19- TinkCryptoManager.kt # AES-256-GCM encrypt/decrypt, X25519 (uses BouncyCastle + JCA)
43+ TinkCryptoManager.kt # AES-256-GCM encrypt/decrypt, X25519 (BouncyCastle + JCA)
2044 data/
2145 local/ # Room DB, DAOs, entities, converters
2246 remote/api/ # ApiService (Retrofit), DTOs
@@ -36,56 +60,106 @@ app/src/main/java/com/kidsync/app/
3660 navigation/ # NavGraph.kt, Routes.kt (sealed class)
3761 screens/ # auth/, calendar/, dashboard/, expense/, family/, settings/
3862 theme/ # Color, Type, Theme (Material 3 + dynamic colors)
39- viewmodel/ # AuthViewModel, CalendarViewModel, ExpenseViewModel, FamilyViewModel, SettingsViewModel
63+ viewmodel/ # AuthVM, CalendarVM, ExpenseVM, FamilyVM, SettingsVM
4064```
4165
42- ## Commands
66+ ## Testing (881+ tests)
4367
44- | Command | Purpose |
45- | ---------| ---------|
46- | Open ` android/ ` in Android Studio | IDE setup |
47- | ` ./gradlew test ` | Run unit tests |
48- | ` ./gradlew assembleRelease ` | Build release APK (requires signing config) |
49- | ` ./gradlew assembleDebug ` | Build debug APK |
68+ All tests are unit tests using JUnit, Mockito/MockK, and Kotlin coroutines test.
5069
51- ## Architecture Layers
70+ Test pattern: mock repositories/DAOs, inject into use case or ViewModel, assert StateFlow emissions.
5271
53- ```
54- UI (Compose screens + ViewModels)
55- ↓ StateFlow + collectAsStateWithLifecycle
56- Domain (use cases, models, repository interfaces)
57- ↓ suspend functions
58- Data (Room DAOs, Retrofit API, repository impls, SyncWorker)
59- ↓
60- Crypto (BouncyCastle + JCA: AES-256-GCM, X25519, HKDF, BIP39)
61- ```
72+ ## Code Style
6273
63- All ViewModels: ` @HiltViewModel ` , ` viewModelScope ` , ` MutableStateFlow ` /` StateFlow `
74+ - ` @HiltViewModel ` + ` viewModelScope ` + ` MutableStateFlow ` /` StateFlow ` for all ViewModels
75+ - ` collectAsStateWithLifecycle ` in Composables (never ` collectAsState ` )
76+ - Clean architecture: UI -> Domain -> Data (repositories bridge the layers)
77+ - ` SharedPreferences.commit() ` for security-critical state, ` .apply() ` only for non-critical
78+ - Sealed class ` Routes ` with ` data object ` entries for navigation
79+ - ` popUpTo ` with ` inclusive = true ` at auth flow transitions
6480
65- ## Critical Patterns
81+ ## Security
6682
67- ** AAD construction** : ` CryptoManager.buildPayloadAad(familyId, deviceId, deviceSequence, keyEpoch) ` -> ` "familyId|deviceId|deviceSequence|keyEpoch" `
83+ - ** E2E encryption:** AES-256-GCM with X25519 key agreement, HKDF key derivation
84+ - ** Key storage:** Android Keystore for session keys, SQLCipher for Room DB encryption
85+ - ** Recovery:** BIP39 mnemonic seed phrase (12 words)
86+ - ** Nonce:** Random 12 bytes prepended to ciphertext: ` nonce(12) || ciphertext || tag ` , then Base64
87+ - ** AAD:** ` CryptoManager.buildPayloadAad(familyId, deviceId, deviceSequence, keyEpoch) ` -> ` "a|b|c|d" `
88+ - ** Hash chain:** ` HashChainVerifier.computeHash(prevHash, encryptedPayload) ` = ` SHA256(hexDecode(prevHash) + base64Decode(encryptedPayload)) `
89+ - ** FLAG_SECURE** on sensitive screens
90+ - ** Key zeroing:** Caller owns the key -- don't zero input parameters in decrypt methods
6891
69- ** Nonce ** : Random 12 bytes prepended to ciphertext: ` nonce(12) || ciphertext || tag ` , then Base64-encoded
92+ ## Critical Patterns
7093
71- ** Hash chain** : ` HashChainVerifier.computeHash(prevHash, encryptedPayload) ` = ` SHA256(hexDecode(prevHash) + base64Decode(encryptedPayload)) `
94+ ** Architecture layers:**
95+ ```
96+ UI (Compose screens + ViewModels)
97+ -> StateFlow + collectAsStateWithLifecycle
98+ Domain (use cases, models, repository interfaces)
99+ -> suspend functions
100+ Data (Room DAOs, Retrofit API, repository impls, SyncWorker)
101+ -> Crypto (BouncyCastle + JCA)
102+ ```
72103
73- ** Navigation** : Sealed class ` Routes ` with ` data object ` entries. ` popUpTo ` with ` inclusive = true ` at auth flow transitions.
104+ ** Sync backends** (all transfer already-encrypted ops -- zero-knowledge maintained):
105+ - Server relay (default): REST API push/pull
106+ - File export/import: ZIP ` .kidsync ` bundles via SAF
107+ - WebDAV/NextCloud: OkHttp PROPFIND/MKCOL/PUT/GET, WorkManager periodic
108+ - P2P local: Google Nearby Connections, HMAC-SHA256 handshake
109+
110+ ## Examples
111+
112+ Good -- ViewModel with proper state management:
113+ ``` kotlin
114+ @HiltViewModel
115+ class ExampleViewModel @Inject constructor(
116+ private val useCase : ExampleUseCase ,
117+ ) : ViewModel() {
118+ private val _state = MutableStateFlow (ExampleUiState ())
119+ val state: StateFlow <ExampleUiState > = _state .asStateFlow()
120+
121+ fun onAction () {
122+ viewModelScope.launch {
123+ _state .update { it.copy(loading = true ) }
124+ useCase.execute().fold(
125+ onSuccess = { _state .update { s -> s.copy(data = it, loading = false ) } },
126+ onFailure = { _state .update { s -> s.copy(error = it.message, loading = false ) } },
127+ )
128+ }
129+ }
130+ }
131+ ```
74132
75- ## Signing (Release)
133+ Bad -- bypassing repository, direct DAO in ViewModel:
134+ ``` kotlin
135+ @HiltViewModel
136+ class BadViewModel @Inject constructor(
137+ private val dao : ExampleDao , // Should use repository/use case
138+ ) : ViewModel() { /* ... */ }
139+ ```
76140
77- Reads from env vars or ` local.properties ` :
78- - ` KIDSYNC_KEYSTORE_PATH ` / ` keystore.path `
79- - ` KIDSYNC_KEYSTORE_PASSWORD ` / ` keystore.password `
80- - ` KIDSYNC_KEY_ALIAS ` / ` key.alias `
81- - ` KIDSYNC_KEY_PASSWORD ` / ` key.password `
141+ ## Checklist
82142
83- Only activates when all 4 values present.
143+ Before committing Android changes:
144+ - [ ] ` ./gradlew test ` passes
145+ - [ ] ViewModels use ` @HiltViewModel ` + ` StateFlow ` (not LiveData)
146+ - [ ] Composables use ` collectAsStateWithLifecycle ` (not ` collectAsState ` )
147+ - [ ] Security-critical SharedPreferences use ` .commit() ` (not ` .apply() ` )
148+ - [ ] Crypto key zeroing: caller owns the key, don't zero input params
149+ - [ ] New screens with sensitive data use ` FLAG_SECURE `
84150
85151## Known Tech Debt
86152
87153- CalendarViewModel is 743 lines (SRP violation, should split)
88- - ExpenseViewModel bypasses repository, injects DAO directly
89154- Monolithic UI state objects (AuthUiState: 16 fields, CalendarUiState: 25 fields)
90155- In-memory expense filtering instead of SQL WHERE
91156- No sync status UI indicator (offline/syncing/synced)
157+
158+ ## When Stuck
159+
160+ 1 . Check ` di/ ` modules for what's provided by Hilt (DatabaseModule, NetworkModule, CryptoModule, AppModule)
161+ 2 . Check ` domain/repository/ ` interfaces for available data operations
162+ 3 . Check ` data/local/dao/ ` for Room query patterns
163+ 4 . Check ` ui/navigation/Routes.kt ` for all navigation destinations
164+ 5 . Check ` crypto/CryptoManager.kt ` interface for encryption API
165+ 6 . Run ` ./gradlew test ` -- failures give clear assertion messages
0 commit comments