Skip to content

Android v1: native Kotlin engine + Compose UI, sideloadable APK - #3

Open
seanrreid wants to merge 10 commits into
mainfrom
android
Open

Android v1: native Kotlin engine + Compose UI, sideloadable APK#3
seanrreid wants to merge 10 commits into
mainfrom
android

Conversation

@seanrreid

Copy link
Copy Markdown
Owner

Summary

Adds the Android build of Seanboy (android/) — native Kotlin + Jetpack Compose, local-first, on-demand R2 sync — plus its PRD. Implements the plan in docs/PRD-android-v1.md through a functional, sideloadable debug APK.

Built and verified locally on JDK 17 + Android SDK 34:

cd android
./gradlew :core:test          # 67 tests, 0 failures
./gradlew :app:assembleDebug  # -> app/build/outputs/apk/debug/app-debug.apk

The APK is com.torchcodelab.seanboy.debug (~15 MB, launchable "Seanboy").

The engine (:core) — pure Kotlin, 67 passing JVM tests

Faithful clean-room port of mac/Sources/SeanboyCore (no Swift reuse path — the port is validated against the same behavior and, where they exist, the same test vectors):

Area What Tests
Document format NoteDocument / Note — frontmatter round-trip, Obsidian preservation, adoption 8
Local storage NoteStore + TombstoneStore — recursive scan, rename, external-change reconciliation, app-private deletes 17
Links & search WikiLinkParser, SearchService — links, backlinks, ranked search 9
Signing SigV4matches the AWS worked examples byte-for-byte (the PRD's flagged highest-risk port) 4
Merge SyncPlanner — three-way merge: conflicts, clock skew, renames, tombstones, resurrection 19
Networking & orchestration S3Client (OkHttp/SAX) + SyncEngine, exercised against MockWebServer with real push/pull round-trips; SyncState persistence 10

The app (app/)

Single-Activity Compose UI wiring the engine:

  • Searchable notes list, editor with backlinks and [[wiki link]] navigation (creates on dead link).
  • Encrypted Sync settings (credentials in EncryptedSharedPreferences / Keystore).
  • On-demand sync on resume — battery-friendly, per the PRD.
  • App-private storage, INTERNET the only permission.

Design decisions (confirmed during PRD)

  • Native Kotlin + Compose (not Flutter) — honors the "one native app per platform" thesis; smallest APK.
  • Local-first cache in app-private storage, text mirrored locally with cloud attachments a later, staged concern — full parity needs the whole corpus on device for offline search & backlinks.
  • Full parity v1; debug-signed sideload (release signing / F-Droid / OTA out of scope).

Not done yet (polish — documented in android/README.md)

None of these touch the data model or bucket format, so notes still round-trip with the Mac:

  • Sidebar is a flat search list, not a collapsible folder tree.
  • Editor is a plain text field, not live Markdown styling.
  • Wiki links follow via a button row, not inline taps.
  • Debounce tuning and QR credential handoff remain.

⚠️ The UI compiles and builds but was not run on a device/emulator in this environment, so runtime behavior is unverified — worth a smoke test.

Notes for reviewers

  • The Mac-only LegacyMigration (old uuid.md files) is intentionally not ported — an Android non-goal; the phone starts fresh.
  • minSdk 26 is a placeholder (open question in the PRD) pending the two target devices' floor.

🤖 Generated with Claude Code

seanrreid and others added 10 commits July 19, 2026 12:33
Native Kotlin + Jetpack Compose app in android/, full parity with the
Mac (search, wiki links, backlinks, R2 sync with three-way merge), a
local-first app-private cache, and a debug-signed sideload APK. Adds the
Android row to the root Platforms table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scaffold android/ per docs/PRD-android-v1.md: Gradle Kotlin DSL with a
version catalog (AGP 8.5, Kotlin 2.0, Compose BOM), a single-Activity
Compose app module (MainActivity + Material3 DayNight theme + placeholder
screen), app-private storage posture (no storage/INTERNET permissions
yet), an adaptive launcher icon placeholder, and the Gradle 8.7 wrapper.

Builds to an installable debug APK (com.torchcodelab.seanboy.debug).
Requires JDK 17+ and SDK 34 to compile — documented in android/README.md.
The :core engine port and INTERNET/sync land in Milestone 1+.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the pure-Kotlin, Android-free :core module (mirrors
mac/Sources/SeanboyCore) with a faithful port of Note/NoteNaming and
NoteDocument: YAML-frontmatter parse/serialize, managed-key handling
(id/seanboy-id/created/modified/deleted), byte-faithful preservation of
unmanaged Obsidian frontmatter, foreign-id demotion to seanboy-id, and
legacy title read-but-never-write. Timestamps use java.time.Instant
truncated to millis so serialize->parse round-trips exactly.

Ports the full NoteDocumentTests suite (8 tests) to JUnit4. NOTE: not yet
compiler-verified in this environment (needs JDK 17 + SDK 34); run
`./gradlew :core:test` to confirm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the rest of the pure-logic engine to :core, verified on JDK 17:
- WikiLinkParser: wiki-link extraction, case-insensitive dedupe, backlinks
- SearchService: ranked term search (title-prefix > title > body)
- SigV4: AWS request signing with javax.crypto -- matches the AWS worked
  examples byte-for-byte (the PRD's highest-risk port)
- SyncState + SyncPlanner: the pure three-way merge (in-memory state; JSON
  persistence follows with sync integration)

Ports the SigV4, WikiLink/Search, and SyncPlanner test suites. Full run:
40 tests, 0 failures (NoteDocument 8, SigV4 4, SyncPlanner 19, WikiLink 5,
Search 4) via gradlew :core:test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the local-first storage layer to :core, adapted for Android's
app-private folder (no system Trash -- delete removes the file and records
a tombstone outside the notes folder):
- NoteStore: recursive scan, filename-as-title, adoption of foreign
  Markdown (id injection), rename-on-title-edit (with case-only hop),
  reload reconciliation (external edit/rename/delete matched by id),
  wiki-link resolution across folders, applyRemote for sync.
- TombstoneStore: per-device deleted-note records with JSON persistence
  matching the Mac format.

The Mac-only LegacyMigration (old uuid.md files) is intentionally not
ported -- an Android non-goal; the phone starts fresh.

Full run: 57 tests, 0 failures (adds NoteStore 14, NoteNaming 3) via
gradlew :core:test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the minimal S3 client to :core: list (continuation-token paging),
get, put (If-Match / If-None-Match conditional writes), delete
(idempotent), copy -- signed with the already-verified SigV4 port. OkHttp
replaces URLSession (works on both JVM and Android); SAX replaces
Foundation XMLParser for ListObjectsV2 and error bodies.

Adds OkHttp + mockwebserver deps and 6 MockWebServer tests exercising
request shape, signing headers, conditional-write failures, and XML
parsing -- coverage the Mac side never had.

Full run: 63 tests, 0 failures via gradlew :core:test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the engine end-to-end in :core:
- SyncEngine: UI-free port of SyncService's sync sequence -- list notes +
  tombstones, download unseen objects, plan via SyncPlanner, upload with
  copy-on-overwrite versioning (pruned to a cap), apply remote changes
  (stashing displaced local edits to .versions/), delete obsolete keys,
  retry 412-blocked uploads. Debounce/observable status are left to the
  Android ViewModel.
- SyncState JSON persistence via kotlinx.serialization (device-local file).

Adds two MockWebServer integration tests driving a real NoteStore +
planner + S3Client through push and pull round-trips, plus SyncState
round-trip tests. Full run: 67 tests, 0 failures via gradlew :core:test.

The :core engine (document, store, tombstones, wiki links, search, SigV4,
three-way planner, S3 client, sync orchestration) is now complete and
test-verified. Remaining PRD work is the Compose UI + app assembly, which
needs Android SDK 34 to build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the :core engine into a functional single-Activity Compose app:
- NotesViewModel: reactive search results, selected note, backlinks, and
  on-demand sync over the store + SyncEngine (engine work off the main
  thread via coroutines).
- SeanboyApp UI: searchable notes list, editor with title/body plus a
  Links/Backlinks row that follows [[wiki links]] (creating on dead link),
  and an encrypted Sync settings screen. Sync fires on resume.
- CredentialStore: R2/S3 config in EncryptedSharedPreferences (Keystore).
- AppContainer: manual DI over the app-private files dir.

Adds INTERNET permission, coroutines, lifecycle-viewmodel-compose,
security-crypto, and the Material Components theme; exposes OkHttp as api
from :core (S3Client's public constructor).

Verified with `./gradlew :core:test :app:assembleDebug` on JDK 17 + SDK 34:
67 tests pass and app-debug.apk builds
(com.torchcodelab.seanboy.debug, ~15 MB, launchable).

Docs: android/README + root Platforms row updated; remaining follow-ups
(folder tree, live Markdown styling, inline link taps, QR handoff) noted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the placeholder adaptive-icon vectors with the real Tomboy
note+pencil artwork extracted from mac/Resources/AppIcon.icns. Foreground
PNGs generated at all five densities (padded into the 108dp adaptive
canvas so the mask never clips the notebook or pencil), over a white
background. Matches the macOS app icon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant