Skip to content

Commit e281c57

Browse files
committed
Document MainViewModel IO no-go
1 parent 07e6670 commit e281c57

5 files changed

Lines changed: 20 additions & 4 deletions

File tree

โ€ŽAGENTS.mdโ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ EasyWatermark (`me.rosuh.easywatermark`) โ€” a privacy-focused Android app that
4545
- **`WaterMarkRepository` now in commonMain (S4d-84..S4d-89, accepted 2026-06-27, final move commit `f23bf5d`):** the watermark-config DataStore consumer moved to `shared/src/commonMain/kotlin/me/rosuh/easywatermark/data/repo/WaterMarkRepository.kt` (same package/FQN) โ€” the **second commonMain DataStore Preferences consumer** after `UserConfigRepository`. It was reached by a staged Android-edge removal (each its own slice, all behavior-preserving): **S4d-84** `Color.parseColor("#FFB800")` โ†’ const `0xFFFFB800.toInt()` (pinned by a Robolectric equality test) + `ArrayMap` โ†’ `mutableMapOf()`; **S4d-85** `java.io.IOException` โ†’ `okio.IOException`; **S4d-86** the localized default text injected as `defaultTextProvider: () -> String` (RepositoryModule passes `context.getString(R.string.config_default_water_mark_text)`, a per-emission lambda); **S4d-87** the persisted tile-id read mapper injected as `tileModeFromStorageId: (Int?) -> WatermarkTileMode` (RepositoryModule passes the **SDK-gated `toWatermarkTileMode`** in `TileModeExt.kt`, so the legacy **pre-Android-12 stored DECAL id 3 โ†’ REPEAT** behavior is preserved โ€” NOT the pure `fromStorageId`); **S4d-88** the one diagnostic `Log.e` injected as `logError: (String) -> Unit` (RepositoryModule passes `Log.e("WaterMarkRepository", message)`, identical tag/message); **S4d-89** the byte-identical file relocation (git rename, 0 content changes). **Persisted bytes/keys unchanged** โ€” `SP_NAME = "sp_water_mark_config"`, all `SP_KEY_*` (incl. `SP_KEY_ICON_URI`, `SP_KEY_CHANGE_LOG`), `MAX_*`/legacy constants, storage ids, and the DataStore file naming + `SharedPreferencesMigration` semantics are identical; **no DataStore migration, no golden rebaseline** (strict 49/0). `RepositoryModule` (the three injected edges) and `DataStoreModule` (still creates the Android `waterMarkDataStore` from `WaterMarkRepository.SP_NAME` via the S4d-74 `createPreferencesDataStore`) are the Android wiring edges; `TileModeExt.kt`/`EditorScreen`/the test resolve the same FQN from `:shared` unchanged. Verified by shared Desktop/iOS/iOS-sim + android + app compile, assembleDebug + assembleRelease (R8 retained the class, APK size unchanged), strict goldens 49/0. These are **code-migration slices, not UI-parity slices**.
4646
- **Room/templates now in commonMain (S4d-90 readiness โ†’ S4d-91 toolchain proof โ†’ S4d-92 move, accepted + committed `8d245d9`):** the production templates Room path moved to `:shared/commonMain` at the **same FQNs** โ€” `data/model/entity/Template`, `data/db/DateConverter`, `data/db/dao/TemplateDao`, `data/db/AppDatabase` (now `@ConstructedBy(AppDatabaseConstructor::class)` + `@Suppress("KotlinNoActualForExpect") expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase>`), and `data/repo/TemplateRepository`. **S4d-90** mapped readiness (Room is atomic; prepopulated-asset data-risk). **S4d-91** proved the Room KMP toolchain (Room Gradle plugin + KSP-MP, `kspAndroid`/`kspDesktop`/`kspIosArm64`/`kspIosSimulatorArm64`) compiles/links for all 4 targets, with the proof Room/SQLite **`compileOnly` in commonMain** so the unused `libsqliteJni.so` did **not** ship to `:app` (r1 confinement; proof package since deleted). **S4d-92** did the production move. **Android driver decision = compatibility mode (no explicit `SQLiteDriver`)** โ€” Android keeps the framework SupportSQLite open-helper + `createFromAsset`, so the prepackaged `ewm-db-ch.db`/`ewm-db-eng.db` open byte-identically and **no** `sqlite-bundled`/`libsqliteJni.so` (or extra `sqlite-framework`) ships; release APK unchanged. Android creation lives in `:shared/androidMain` `data/db/TemplateDatabaseBuilder.android.kt` `buildTemplateDatabase(context)` (locale `createFromAsset` + in-memory fallback, same `ewm-db` path); `AppModule`/`RepositoryModule` call it (`TemplateRepository` now takes an injected `ioContext: CoroutineContext` โ€” Koin passes `Dispatchers.IO`, since `Dispatchers.IO` isn't accessible in commonMain on Native). `:shared` commonMain Room dep is `implementation(room.runtime)` (no sqlite artifact); `sqlite-bundled`/`sqlite` removed from the catalog; `app/build.gradle.kts` untouched (its existing room deps cover the `RoomDatabase` supertype). `exportSchema=false`, `version=1` unchanged; no schema/migration change, no golden rebaseline. Proven by a Robolectric **prepopulated-DB smoke** (`TemplatePrepopulatedDbSmokeTest`, 2/0 โ€” both seed assets open + read seeded templates non-empty), all-target compile, iOS link, strict goldens 51/0, debug+release APK (no `libsqliteJni.so`). **Desktop/iOS production templates builders are intentionally unwired** (the DB type compiles/links on those targets but no builder is created โ€” no templates UI there yet). **Residual:** the release APK was not run on a device (release retention is covered by R8 keep-rule analysis + the debug smoke). Code-migration slices, not UI-parity.
4747
- **First shared editor use-case extracted (S4d-94 โ†’ S4d-95 โ†’ S4d-96, accepted; S4d-96 committed `7ee7e77`):** **S4d-94** (read-only) decided the **Koin common/platform split stays deferred** โ€” no non-Android DI consumer exists (Koin is started only in `MyApp`; desktop/iOS construct their one repo manually), so a commonMain Koin module would be a framework layer with no consumer. **S4d-95** (read-only) mapped the ViewModels and found `MainViewModel` cannot move (deep `Bitmap`/`Uri`/`MediaStore`/`File`/`Context` IO) and both VMs extend Android `androidx.lifecycle.ViewModel`; the smallest non-speculative step is a commonMain **use-case** consumed by the Android VM. **S4d-96** landed it: `shared/src/commonMain/.../domain/WatermarkConfigEditor.kt` โ€” the **first small shared editor config use-case** over the commonMain `WaterMarkRepository`, with the 12 `update*` config edits as `suspend` methods and the three inline rules moved **verbatim** (text-size `coerceAtLeast(0f)`, `WatermarkConfigRules.alphaPercentToByte`, non-empty `MediaRef` icon guard). It is a **behavior-preserving extraction, NOT a shared ViewModel or reducer**: `MainViewModel` stays Android-side, still owns `viewModelScope`/`launch` and the save/compress/gallery/picker/crash/IO flows, and constructs the use-case from the already-injected `WaterMarkRepository` (**no Koin/AppModule/Gradle/lifecycle/renderer/storage change**). Public VM method names/signatures unchanged. Verified: all-target compile + `:app`, `:shared:linkDebugTestIosSimulatorArm64`, strict goldens **51/0** (no rebaseline), debug+release APK (release size unchanged), daemon stop. **S4d-97 (accepted, committed `ec9317c`)** added the sibling `shared/commonMain/.../domain/OutputPrefsEditor.kt` โ€” `class OutputPrefsEditor(repo: UserConfigRepository)` with one `suspend save(format, level)` wrapping `UserConfigRepository.updateFormat(format)` then `updateCompressLevel(level)` (same order, no validation/clamping added); `MainViewModel.saveOutput` keeps its signature/defaults and still owns `viewModelScope.launch` with `resetJobStatus()` after the launch/outside the coroutine, built from the already-injected `userRepo` (no DI change). Same gates green (51/0, release size unchanged). **S4d-98 (accepted, committed `d26c95b`)** added the third sibling `shared/commonMain/.../domain/TemplateEditor.kt` โ€” `class TemplateEditor(repo: TemplateRepository)` with `isDaoNull()` (wraps `checkIfIsDaoNull()` so the VM keeps the null-DAO โ†’ `UiState.DatabaseError` mapping Android-side โ€” `UiState` is **not** in commonMain), `suspend add(content)` constructing `Template(0, content, Clock.System.now(), Clock.System.now())` (two separate `now()` calls, verbatim) then inserting, and `suspend update/delete` delegations. `MainViewModel` keeps its public signatures + the synchronous null-DAO-check structure, owns `viewModelScope`/`launch`/`UiState`, built from the already-injected `templateRepo` (no DI change). A **cheap** commonTest (`TemplateEditorTest`, 2/0) pins `isDaoNull()` true/false (possible here because `TemplateDao` is an interface); the suspend writes are NOT directly unit-tested (commonTest has no coroutine runner) โ€” compile/link + unchanged goldens are the build/regression net, not a unit test of those writes. The three small editor use-cases are now extracted; the remaining shared-state work (the deliberate multiplatform-`lifecycle` decision for moving `AboutViewModel`, and real Desktop/iOS editor UI consumers) stays separate.
48-
- **NOT yet done** (the bulk โ€” months of work): the broader shared ViewModel/use-case extraction โ€” the three small shared editor use-cases landed (`WatermarkConfigEditor` S4d-96, `OutputPrefsEditor` S4d-97, `TemplateEditor` S4d-98), but `MainViewModel`/`AboutViewModel` themselves stay Android-side โ€” **S4d-99 (read-only readiness, accepted)** found `AboutViewModel` is technically ready but owner-gated/speculative until a real Desktop/iOS About consumer exists; the **golden test harness (C1.7)** that gates deeper engine rewrite (C2); the Koin common/platform split (deliberately deferred by S4d-94 while no non-Android DI consumer exists); iOS/Desktop seed-DB packaging/default-template parity beyond the Android Room compatibility-mode move (S4d-90..S4d-92); the full Compose Desktop editor beyond the current minimal product-flow window; Desktop renderer feature parity; reactive preview/editor polish; and the remaining real PHPicker grid-cell proof. Android text/icon/composition/decode production remains native by default. KMP/CMP migration is NOT complete.
48+
- **NOT yet done** (the bulk โ€” months of work): the broader shared ViewModel/use-case extraction remains consumer-gated โ€” the three small shared editor use-cases landed (`WatermarkConfigEditor` S4d-96, `OutputPrefsEditor` S4d-97, `TemplateEditor` S4d-98), but `MainViewModel`/`AboutViewModel` themselves stay Android-side. **S4d-191 (read-only readiness, accepted)** confirmed there is no safe non-speculative `MainViewModel` business-IO slice right now: the consumed business logic is already shared, remaining pure-looking methods are Android `UiState` only, and the rest is Android IO/render (`ContentResolver`/`MediaStore`/`Bitmap`/`Canvas`/`Compressor`/native `WatermarkRenderer`). Do NOT create a shared ViewModel/navigation reducer/IO `expect` layer without a named real off-Android consumer or an explicit owner decision. **S4d-99 (read-only readiness, accepted)** found `AboutViewModel` is technically ready but owner-gated/speculative until a real Desktop/iOS About consumer exists; the **golden test harness (C1.7)** that gates deeper engine rewrite (C2); the Koin common/platform split (deliberately deferred by S4d-94 while no non-Android DI consumer exists); iOS/Desktop seed-DB packaging/default-template parity beyond the Android Room compatibility-mode move (S4d-90..S4d-92); the full Compose Desktop editor beyond the current minimal product-flow window; Desktop renderer feature parity; reactive preview/editor polish; and the remaining real PHPicker grid-cell proof. Android text/icon/composition/decode production remains native by default. KMP/CMP migration is NOT complete.
4949

5050
Session memory (planning-with-files): `task_plan.md`, `findings.md`, `progress.md` at repo root โ€” read them at session start, update them as you work.
5151

@@ -63,7 +63,7 @@ Session memory (planning-with-files): `task_plan.md`, `findings.md`, `progress.m
6363
## Architecture (big picture)
6464

6565
- Modules: `:app` (everything), `:cmonet` (Material You dynamic-color gate, Android-only โ€” **now fronted by the platform-neutral `DynamicColorCapability`** for the live Compose consumers (S4d-43, accepted); `:cmonet` is **retained** behind the Android actual, full absorption/removal is an owner-gated follow-up โ€” ADR-0007), `:baseBenchmarks`/`:macrobenchmark` (Android-only perf), `buildSrc`.
66-
- **Data flow:** DataStore Preferences (watermark config via `WaterMarkRepository` and user prefs via `UserConfigRepository` โ€” **both now commonMain** (S4d-84..S4d-89 and S4d-77 respectively), with their Android-only behavior injected at the `RepositoryModule` Koin edge; **store creation** is seamed into `:shared/androidMain` `createPreferencesDataStore(context, name)` since S4d-74, with `di/DataStoreModule.kt` keeping the `Context.userDataStore`/`waterMarkDataStore` property names) + Room (`Template` entity + `AppDatabase`/`TemplateDao`/`TemplateRepository` **now commonMain** per S4d-90..S4d-92; Android creation in `:shared/androidMain` `buildTemplateDatabase` keeps the **prepopulated DBs** `assets/ewm-db-ch.db`/`ewm-db-eng.db` selected by locale, in **Room compatibility mode** โ€” no explicit `SQLiteDriver`, framework SupportSQLite, no `sqlite-bundled` native payload) โ†’ Koin DI (`di/`) โ†’ `MainViewModel` (large Android-side state/workflow owner; source state is now StateFlow-only after S4d-63..S4d-69; the first shared editor use-case `WatermarkConfigEditor` was extracted in S4d-96, but the VM itself stays Android-side and the broader shared ViewModel/use-case extraction is still ahead) โ†’ both UI stacks.
66+
- **Data flow:** DataStore Preferences (watermark config via `WaterMarkRepository` and user prefs via `UserConfigRepository` โ€” **both now commonMain** (S4d-84..S4d-89 and S4d-77 respectively), with their Android-only behavior injected at the `RepositoryModule` Koin edge; **store creation** is seamed into `:shared/androidMain` `createPreferencesDataStore(context, name)` since S4d-74, with `di/DataStoreModule.kt` keeping the `Context.userDataStore`/`waterMarkDataStore` property names) + Room (`Template` entity + `AppDatabase`/`TemplateDao`/`TemplateRepository` **now commonMain** per S4d-90..S4d-92; Android creation in `:shared/androidMain` `buildTemplateDatabase` keeps the **prepopulated DBs** `assets/ewm-db-ch.db`/`ewm-db-eng.db` selected by locale, in **Room compatibility mode** โ€” no explicit `SQLiteDriver`, framework SupportSQLite, no `sqlite-bundled` native payload) โ†’ Koin DI (`di/`) โ†’ `MainViewModel` (large Android-side state/workflow owner; source state is now StateFlow-only after S4d-63..S4d-69; shared editor use-cases were extracted in S4d-96..S4d-98, and S4d-191 keeps the rest of this VM Android-side until a real off-Android consumer exists) โ†’ both UI stacks.
6767
- **Rendering engine (the product core):** `WatermarkRenderer` (Android-only seam, `:app/render`) builds a watermark "cell" offscreen (text via StaticLayout, icon via scaled bitmap, rotated), wraps it in a `BitmapShader` โ€” `REPEAT` tiles it across the photo, `CLAMP` ("decal") draws one draggable instance at a fractional offset. Both the Compose `Canvas` preview (`EditorScreen.WaterMarkCanvas`) and export (`MainViewModel.generateImage`) call the **same** `WatermarkRenderer.build*Shader` + `compose` seam, so they composite identically. Cell sizing is **image-space** (S3a: `textPx = textSize * imageWidth / REF_WIDTH`, ref width 1000), independent of any view matrix โ€” the old `ViewInfo`/`1/MSCALE_X` export-scale coupling was removed (S3c-1/S3c-3). The shared sizing **constants/math now live in commonMain** and are consumed by all three platforms: text image-space sizing via `WatermarkGeometry.REF_WIDTH` / `WatermarkGeometry.fontPx(textSize, imageWidth)` (Android `PainKtx.applyConfig`, Desktop, iOS โ€” S4d-181/182), icon scale via `WatermarkCellComposer.ICON_SCALE_REFERENCE_TEXT_SIZE` (Android/Desktop/iOS โ€” S4d-184), and cell *geometry* (gap/diagonal/rotated-AABB) via `WatermarkGeometry`. **Only the constants/math are shared โ€” Android text/icon production raster/draw stays native** (`StaticLayout` / `buildIconShader`; ADR-0004/S4d-8/S4d-17); the `WatermarkCellComposer`/`TextRasterEnv` raster primitives remain the Desktop/iOS path, not Android-production-wired. The legacy Android renderer `REF_WIDTH` const was deleted in S4d-188 (it was orphaned; `WatermarkGeometry.REF_WIDTH` is the live source). commonMain `WatermarkCellComposer` is the **Desktop/iOS** renderer (cell raster + `composeOverBackground` composition/tiling); **Android production raster AND composition stay native by closed decisions** โ€” icon (S4d-8), text (S4d-17), and composition (S4d-190 No-Go) โ€” see ADR-0004.
6868
- **Gotchas encoded in data:** watermark config values and pure normalization rules have been moved toward commonMain without changing stored bytes. `WatermarkTileMode.storageId` still mirrors the historical Android `Shader.TileMode` ordinals; `WatermarkMode.Text/Image` mirrors the deleted `WaterMarkRepository.MarkMode` ints; `TextTypeface`/`TextPaintStyle` keep their old `serializeKey()` values; `WatermarkConfigRules` preserves the legacy clamps/conversions for text size, alpha, h/v gap, degree, and mode transitions. Android render types stay at the `:app` edge via `TileModeExt.kt` / `TextStyleExt.kt`; do not put `android.graphics.*`, `android.net.Uri`, or repo-nested types back into common model classes. **Note:** `WaterMark.iconUri` has already been migrated to the platform-neutral `MediaRef` (`@JvmInline value class`, `shared/commonMain`; needs `import kotlin.jvm.JvmInline`) with Android `Uri` only at the edge (`utils/ktx/MediaRefExt.kt`). **`ImageInfo.uri` and `WaterMarkRepository.imageInfoMap` are now `MediaRef` too โ€” S4d-52, accepted 2026-06-25** (the `Uri โ†” MediaRef` conversion lives at the picker/share-in/gallery construction and the decode/Coil/save edges; `MediaRef` value-class string equality keeps `imageInfoMap`/`isSameItem` semantics). `ImageInfo` itself moved to `shared/commonMain` in S4d-71; the old Android range annotation on `offsetX`/`offsetY` is gone, and the normalized 0f..1f invariant is documentation only. The dead `ImageInfo.shareUri` accessor was **removed (S4d-53, 2026-06-25)** โ€” it had zero call sites (the share-out button is an unwired empty lambda); the real export result stays in `ImageInfo.result`/`jobState`. **Deliberately kept as Android `Uri` edges (do NOT "fix" incidentally):** gallery `Image.uri`, `Action.SystemPickerImageSelected.uriList`, `SaveExportSheet.imageUris`, the picker contracts (`PickImageContract`/`MultiPickContract`), and `BitmapUtils`/`BitmapCache`/`FileUtils` decode signatures. The dead `KEY_URI`/`SP_KEY_URI` DataStore key declarations were **removed (S4d-54, 2026-06-25)** โ€” `KEY_ICON_URI` and all real watermark prefs are untouched. **No model-layer `Uri` hygiene remains**; the only `Uri` left is the deliberate Android edges above.
6969
- Image IO: decode via `BitmapFactory` + inSampleSize with EXIF rotation baked in (`utils/bitmap/BitmapUtils.kt`); save via MediaStore `IS_PENDING` (API โ‰ฅ29) or pre-Q file path; export deliberately strips all EXIF metadata (privacy feature โ€” ADR-0009).

0 commit comments

Comments
ย (0)