Commit Graph
12 Commits
Author SHA1 Message Date
weishu 4948d23669 fix(android): enforce Google Play compliance 2026-08-25 16:44:03 +08:00
weishu c621389e29 feat(android): read upload-key config from local.properties too
local.properties is the conventional gitignored home for machine-local
secrets, but it is not part of gradle's own property chain — the seam
now loads it explicitly as the third source (gradle property > env >
local.properties, same names). Also imports java.util.Properties at the
top of the script: the bare FQN resolves against the java extension
accessor in Android Kotlin DSL and fails to compile.
2026-08-25 13:31:59 +08:00
weishu a614324f2d feat(android): release upload-key signing seam (properties/env, repo stays secret-free)
Same conditional philosophy as the google-services.json plugin: with no
configuration, :app:bundleRelease builds an unsigned AAB and the repo
needs no secrets; an upload keystore supplied via user-global gradle
properties or env (HAPI_UPLOAD_KEYSTORE + passwords, ~ expanded) signs
release builds for Play App Signing. Verified both paths: unsigned
bundleRelease (first R8 run — builds clean, 8.0MB vs 18.8MB debug) and
a throwaway-keystore signed bundle (jarsigner: jar verified). Also
fixes the stale FCM_PROJECT_ID reference in the README.
2026-08-25 13:31:59 +08:00
weishu 641ae3bf36 feat(android): zh-CN localization + language switching (B-M5a)
Extraction sweep: every user-visible hardcoded English string in
android/app moved to values/strings.xml with feature-prefixed keys
(sessions_/chat_/files_/scratchlist_/pairing_/new_session_/tool_...);
values-zh-rCN/strings.xml translates all 448 keys, terminology aligned
with web/src/lib/locales/zh-CN.ts.

ViewModels stay string-free: transient notices became semantic sealed
types resolved at the UI layer (ChatNotice, ScratchlistNotice +
ScratchlistImportRejection, PairingError, DictationErrorKind,
SessionListError.DeleteFailed.stillActive); ViewModels that genuinely
compose display text take small Strings seams with English defaults
for JVM tests (FilesStrings, FileViewerStrings, NewSessionStrings);
toolCardPresentation gains a Resources parameter.

Language switching: LanguagePrefs gains SYSTEM (follow system, the new
default); Settings applies AppCompatDelegate.setApplicationLocales
immediately; MainActivity now extends AppCompatActivity over
Theme.AppCompat.DayNight.NoActionBar with autoStoreLocales +
android:localeConfig; FCM/WorkManager surfaces wrap the application
context via localizedForAppLanguage (per-app locales miss non-activity
contexts below API 33).

Verification: :app:assembleDebug green, :app:lintDebug 0 errors
(MissingTranslation clean), full JVM test gate green (176 app tests;
notice assertions updated to the semantic types).
2026-08-18 15:54:48 +08:00
weishu bf63d2ad7b feat(android): FCM push + notification actions (B-M4a)
Gradle/Firebase: firebase-messaging + work-runtime-ktx in the catalog and
:app; com.google.gms.google-services applied CONDITIONALLY (only when
app/google-services.json exists) so the repo builds green without any
Firebase config; committed google-services.json.example + README section
(CI-injected official builds / self-build drop-in / v1.x runtime
FirebaseOptions path); real config gitignored. push/PushBinding.kt is the
availability seam: no Firebase -> every push path no-ops.

Registration (:core:data push/DeviceRegistrar): stable DataStore UUID
deviceId; POST /api/devices/register {token, platform:'phone', deviceId}
fanned out to EVERY paired hub on app start, on pairing (roster addition),
and on onNewToken; transient failures retry via a per-hub WorkManager
unique work item; best-effort DELETE on sign-out while the hub's JWT still
works.

Service (fcm/HapiFirebaseMessagingService): data-only contract v1 decoding
in :core:data push/PushPayload — channels permission_requests(HIGH) /
ready / task_notifications (created on app start), type-<sessionId>
coalescing tags, severity accent colors, notifySummary-driven ready
bodies, unknown type/contractVersion degrade to plain title/body (default
channel, no actions). Suppress-when-open: foreground + that session's
chat composed -> skip (SSE already shows it). Tap -> internal MainActivity
intent route (no public URI) -> existing navigation opens the chat.

Actions: permission-request Allow/Deny and ready/task RemoteInput Reply +
Dismiss -> NotificationActionReceiver -> expedited CoroutineWorkers
(approve/deny {} bodies, reply {text, localId}) through on-demand
HubSessions built from stored credentials (HapiWorkerFactory +
Configuration.Provider + on-demand WorkManager init — no HubGraph needed
in background); notification updates pending -> done / "Already handled"
(404 request-gone) / inactive / failed. Multi-hub: payload has no hub URL,
so workers try the ACTIVE hub first, then other paired hubs on 404
session-miss (single-hub users always hit first try).

Hub check: android.priority=HIGH is already set unconditionally for every
FCM message (hub/src/fcm/fcmService.ts) — no hub change needed.

Tests (34 new, :core:data): payload keys/severities/unknown contract
version, channel routing, suppress-when-open; registrar fan-out /
addition-only / retry-on-transient / null-token no-op via fake seams;
action wire bodies + Bearer header + multi-hub resolution through a real
HubSession against two MockWebServers. Full gate green with AND without
google-services.json (protocol 227, data 182, app 96 tests; debug +
release/R8/lintVital assemble).
2026-08-18 10:50:07 +08:00
weishu c164af0738 feat(android): dictation, slash commands, session ops (B-M3ce)
- Voice dictation: DictationController (seam over recorder + api, JVM-tested),
  MediaRecorder m4a/AAC recorder, provider discovery via
  GET /api/voice/transcription/providers (new HapiApi method + wire types),
  mic button + recording chip + RECORD_AUDIO flow in the composer,
  transcript appended with space separator (web appendTranscript twin).
- Slash commands: '/' at start-of-input opens a dropdown merging
  metadata.slashCommands names with GET /slash-commands (RPC wins dedupe,
  exact>prefix>contains filter); tap inserts '/name '. Skills '$' deferred.
- Session ops: SessionStore gains rename (optimistic + roll-forward),
  delete (optimistic + 409 restore), reopen (marks returned id active);
  list long-press sheet + chat top-bar overflow wire Rename/Reopen/Delete;
  chat reopen reuses the supersede path (window seed + draft move +
  ChatEvent.SessionSuperseded); 422 formatted via formatReopenError;
  inactive-session affordance bar above the composer.
- Additive wire/API: TranscriptionProvidersResponse/TranscriptionProviderInfo,
  HapiApi.getTranscriptionProviders, ChatSessionApi.getSlashCommands,
  SessionListStore rename/delete/reopen.
- Manifest: RECORD_AUDIO + microphone uses-feature required=false. README blurb.
- Tests: DictationController (happy/no-provider/errors/cancel), slash
  merge/filter/trigger, store rename/delete/reopen (MockWebServer),
  VM reopen-supersede/delete/rename/slash suggestions; full gate green
  (protocol 227, data 148, app 96 tests; assembleDebug OK).
2026-08-18 10:16:05 +08:00
weishu 3cf2a669c3 feat(android): pairing flow, deep link, DI graphs, navigation skeleton (B-M1d) 2026-08-17 15:39:29 +08:00
weishu f275bae4cb merge: B-M1c Android SSE engine + reconnect state machine
# Conflicts:
#	android/README.md
#	android/core/data/build.gradle.kts
#	android/gradle/libs.versions.toml
2026-08-17 15:16:38 +08:00
weishu 483155ece3 feat(android): SSE engine + reconnect state machine (B-M1c)
:core:data app.hapi.data.sse — the /api/events transport per
docs/api/client-contract/sse.md (reference web/src/hooks/useSSE.ts):

- SseConnection: SseTransport seam (Connected/Event/Failure flow) with the
  okhttp-sse implementation on a dedicated client (readTimeout=0, no cache,
  own dispatcher), buildEventsUrl, Last-Event-ID header on resume, and an
  acceptEncodingIdentity fallback flag for the gzip escape hatch.
- ReconnectPolicy: normative constants + pure backoff schedule (immediate
  first retry, 1s..30s exponential, 300s ceiling after 8 attempts, 0..500ms
  injected jitter).
- SseEngine: per-key (global / session:<id>) connection loops — handshake
  gate on connection-changed{status:connected} with ok/gap resume verdict,
  per-key cursors advanced only after the downstream hand-off
  (at-least-once), 10s connect deadline, 90s staleness watchdog ticking 10s,
  one silent 401 re-auth per cycle costing no backoff attempt, background
  retry deferral + 45s foreground stale check; all timing via injected
  delay/clock for virtual-time tests.
- SyncEventRouter: 13-type union fan-out to the SyncTargets seam (M2 wires
  stores), handshake gap -> requestFullResync, Unknown ignored.

Tests (32): virtual-time engine suite (fake transport + turbine), policy
schedule with seeded jitter, router mapping, and MockWebServer integration
through the real okhttp transport — including proof that gzip SSE frames
surface incrementally (flush-per-event body withheld behind a throttle).
Gzip also verified against a live local hub (Content-Encoding: gzip,
handshake decoded instantly, heartbeat +30.0s mid-stream).
2026-08-17 15:14:55 +08:00
weishu 6f1dc23d77 feat(android): auth + HapiApi transport (B-M1b)
:core:data auth + API transport for track B M1:

- auth/: JwtPeek (unverified {uid,ns,exp} peek for proactive refresh),
  CredentialStore interface + EncryptedPrefsCredentialStore
  (security-crypto) + in-memory impl, HubUrls origin normalization,
  HubRegistry (ordered multi-hub roster + active hub behind a storage
  seam), AuthInterceptor + single-flight TokenAuthenticator (401 ->
  re-exchange -> retry once, Mutex single-flight, terminal AuthEvents,
  ensureFreshToken() for SSE pre-connect)
- api/: HapiApi (plain OkHttp + kotlinx.serialization, one suspend fun
  per v1 endpoint incl. generated-image bytes + multipart transcription
  helper), ApiError with (status, code) per errors.md
- HubSession: per-hub factory wiring clients (timeouts, auth decoration,
  256 MB image cache), DI-free
- :core:protocol wire/: request DTOs (ApiRequests.kt) + response DTOs
  (auth, health, resume/reopen, cancel/steer/queued-state, spawn,
  slash-commands/skills, machine list-directory/paths-exists, uploads,
  transcription)
- tests: 45 unit tests (MockWebServer) — refresh/retry-once, concurrent
  single-flight, terminal 401 paths, ApiError code mapping, request
  shapes (cursors+epoch, deliveryMode, nested answers, explicit nulls),
  JwtPeek/HubUrls/HubRegistry
2026-08-17 13:07:44 +08:00
weishu b09dd67268 feat(android): protocol wire models, catalogs, session patching (B-M1a)
:core:protocol, package app.hapi.protocol — pure-JVM foundations for the
Android client, mirrored from shared/src/schemas.ts + sessionSummary.ts and
docs/api/client-contract/{sse,pagination,messages}.md:

- wire/: shared HapiJson config; DecryptedMessage with tri-state invokedAt
  (custom serializer — explicit null = queued vs absent = legacy-invoked);
  AttachmentMetadata; AgentState + (completed) requests; Session verified
  field-by-field against SessionSchema; typed-subset SessionMetadata;
  SessionSummary/PendingRequest; SessionPatch with VersionedValue wrappers
  and a strict SessionPatches.parse (unknown key / empty / mistyped -> null,
  replicating SessionPatchSchema.strict()); Machine family; the 13-type
  SyncEvent union behind total SyncEvents.parse (unknown/malformed ->
  Unknown, never throws; machine-updated data kept tri-state for the
  removed-vs-refetch distinction); MessagesPage + REST envelopes.
- catalog/: AgentFlavor (known + Other(raw)) with capabilities/labels from
  flavors.ts; PermissionMode/tone table, per-flavor mode lists, codex
  collaboration + copilot agent modes incl. legacy 'fleet' normalization.
- patch/SessionPatching.kt: exact port of web/src/lib/sessionPatch.ts —
  strict > version gates, max-monotonic updatedAt, activeAt <60s
  render-irrelevance, and deliberately NOT applying activeTurnStartedAt /
  scratchlistUpdatedAt (the TS reference never assigns them).

Tests (85 total, all green): golden-fixture decoding over shared/fixtures/
chat/* (fails loudly on zero files or unsupported VERSION), version-gate /
keep-alive cases ported from useSSE.test.ts, full-session + list + messages
page decode, SyncEvent union coverage, catalog pins (TODO(K6): switch to
fixtures/catalogs/modes.json once generated). :app:assembleDebug verified.
2026-08-17 12:06:32 +08:00
weishu 83bd25aa3b feat(android): scaffold Compose app + core modules + CI (B-M0) 2026-08-17 11:22:35 +08:00