Commit graph

33 commits

Author SHA1 Message Date
avi
6cb5797990 Widget instant start: sync consent cache, plain banner with actions
Some checks failed
CI / android (push) Has been cancelled
Cold widget taps waited on TOP-resume (never fires for the
translucent trampoline) plus a DataStore consent read, costing
seconds and spurious app-opens when the read timed out. Fire from
onResume, cache consent in SharedPreferences, keep the first 15s
of banner on the stock template (now with Pause/Save/Discard
actions) until RemoteViews inflation is warm.
2026-10-06 14:31:08 -05:00
avi
c35e6935b5 Checkpoint: current Shonar state
Some checks failed
CI / android (push) Has been cancelled
2026-09-23 18:58:28 -05:00
avi
cd173d80b8 Widget mirrors in-app Record button; consent-gated start, save while active 2026-09-18 18:17:37 -05:00
avi
673330293a Widget trampoline waits for foreground recording; fixes 10s notification delay 2026-09-18 17:24:03 -05:00
avi
1f46d2a0e4 Single-action recording rows, S.H.O.N.A.R. header branding, drop storage pill 2026-09-18 16:11:34 -05:00
avi
47f7cb9336 Reskin to dark-navy + cyan design system; tidy recording rows, settings access on top 2026-09-18 15:32:41 -05:00
avi
9c866c858b Record FAB paints with accent (primary); Material3 default primaryContainer muted it
- Theme: derive primaryContainer from accent in both schemes (other container-tinted surfaces follow accent now)
- Home FAB: explicit containerColor=primary so button color == accent_color exactly
- Settings: hide 'Reset to default' on non-editable settings (App version showed it and threw 'not editable')
2026-09-17 22:06:38 -05:00
avi
b6d4fd3cb2 Theme + accent settings now apply live (were decorative: ShonarTheme ignored them) 2026-09-17 21:25:20 -05:00
avi
c942865879 Notification channel v3: HIGH + 60ms tick, lockscreen public (silent channel delayed the heads-up pill) 2026-09-17 20:51:19 -05:00
avi
8489c407cc Widget tap: invisible trampoline with own task affinity (was revealing MainActivity under it) 2026-09-17 20:51:19 -05:00
avi
7f9b9c2e5c Widget: render mic+badge as bitmaps (Lawnchair themed-icons flattened drawable resources to gray) 2026-09-17 20:51:19 -05:00
avi
d8441ab385 Notification text follows system theme (fix white-on-white in light mode)
The custom RemoteViews layouts hard-set @color/shonar_widget_text (white)
for the dark navy card, but SystemUI paints the notification card with the
SYSTEM theme: in light mode the card is pale lavender and the title,
file name, and timer rendered white-on-white.

Add @color/notif_text = ink by default, #E6EDF3 in values-night, and use
it in notification_recording, notification_recording_small, and
notification_confirm_delete. The widget keeps its own white text (it paints
its own navy background).

On-device (Pixel 8 Pro, night=no): recording card text is dark-on-light;
zero white text pixels in the card region (was washed-out white before).
2026-09-17 13:48:43 -05:00
avi
645e466b2f Instant record banner from widget tap; fix ghost notification + main-thread recorder prep
- ensureForeground() posts the FGS banner at the top of onStartCommand on a
  cold-start START only (was: every action -> raced stopForeground(REMOVE)
  and re-flipped the discard-confirm card back to the plain card)
- MediaRecorder prepare()/start() moved to Dispatchers.IO (StrictMode:
  disk I/O was on the main thread); start failure reverts UI
- discardRecording() tears the banner down synchronously before stopSelf()
  so a scope cancel in onDestroy can't leave a ghost notification
- ensureChannel() memoized + legacy channel cleanup kept (one IPC pass per
  process instead of per call)
- restore migrateRestoreTimes() + runCatching around refreshRoot() in
  Application.onCreate (dropped accidentally during timing instrumentation)
- Home screen: Column+verticalScroll -> LazyColumn with keyed items
- Notification layouts: explicit textColor for lockscreen/shade legibility
- Widget tap PendingIntent: FLAG_RECEIVER_FOREGROUND for prompt delivery
- All ShonarTiming instrumentation probes removed

On-device (Pixel 8 Pro): widget tap -> banner ~195ms; recording file grows
in .in-progress/; Discard->No survives, Discard->Yes removes service, temp,
and notification with nothing left live; no StrictMode recorder violation.
2026-09-17 13:23:38 -05:00
avi
1d53d0f00c Notification: drop duplicate system No/Yes on delete-confirm card
- buildConfirm had card buttons + addAction row: shade showed
  green No/Yes plus black-text No/Yes pair (seen 2026-09-15)
- Keep only the card buttons; no system actions on any recording card
- Test now asserts zero system actions
2026-09-15 14:39:19 -05:00
avi
ffea797eba Settings: remove search field
- Settings list is small; search added noise
- Drop OutlinedTextField + query filtering from ViewModel
2026-09-15 11:34:54 -05:00
avi
a005d4d922 Home: add Discard with confirm to in-progress recording card
- RecordingStatusCard: Pause/Resume + Finish + Discard (red)
- Discard shows confirm dialog ('Delete current recording?')
  then discards temp via ACTION_DELETE; Cancel keeps recording
2026-09-15 10:41:17 -05:00
avi
5235707de5 Library folder setting + title-time restore + notification heads-up/lockscreen + version stamp
- Settings: editable Library folder (blank = default Music/Recordings);
  bad/unusable paths keep previous folder, Current path shown, toast on update/reset
- Repository: refreshRoot never resets behind user; migrateRestoreTimes()
  restores HH-mm from createdAt for date-only titles, handles (2) collisions, once
- Notification: fresh v2 channel IMPORTANCE_HIGH (banner on record start),
  PRIVATE + publicVersion for secure lockscreen (single mic row, no pills)
- Build: versionCode = git commit count, versionName 0.1.<count>
- README: library folder note
- Tests: LibraryRootTest, TitleRestoreTest, updated layout + settings tests
2026-09-15 10:41:17 -05:00
avi
8cd8861695 Recording notification: correct card, fast, no pop-banner
- Post the foreground notification once on start (skip the duplicate
  nm.notify that restarted SystemUI RemoteViews inflation).
- Collapsed shade row is a slim header-only card (lock shade clips
  taller cards); expanded keeps the pills card. Restore
  DecoratedCustomViewStyle (without it the system drops both custom
  views and expansion breaks) plus a custom heads-up view.
- Fallback template leads with the timer; drop the SHONAR subtext
  ('SHONAR x SHONAR' header) and the system action row duplicating
  the pills.
- Recording channel is DEFAULT: no heads-up banner pinning the shade
  item expanded on every record start.
- Regression tests lock each behavior in
  RecordingNotificationLayoutTest.
2026-09-15 05:56:34 -05:00
avi
d68aab5336 Playback you can always stop + release build + storage-access prompt
Phantom audio (playing with no UI and no way to stop it) is gone:
- New PlaybackController owns the single MediaPlayer process-wide.
  Home rows and details mirror the same state; a generation counter
  ignores stale async-prepare callbacks from rapid re-taps.
- Ongoing 'Now playing' notification with a Stop action works even
  with the app closed (receiver clears card + releases player).
- Details keeps seek/speed UI on top of the shared player.

Fluidity (measured on-device, release build):
- StrictMode caught 192ms DateFormat disk I/O inside composition
  (details info card) + library scans on the main thread. Date is
  pre-formatted in the ViewModel on IO; scans moved to Dispatchers.IO.
- Detail-open on release: 84 frames, 1 janky (1.2%), p99 31ms
  (was 6-8 janky, p99 ~130ms on debug).

Also: storage-access prompt card (fresh installs lose the file
grant; guides to system Settings), release signing via local
keystore (passwords in ~/.gradle/gradle.properties, key ignored
by git), default save name is now 'Recording - <date>'.
2026-09-14 14:30:04 -05:00
avi
a445105d86 Android: optional local release signing (keystores ignored; passwords via ~/.gradle/gradle.properties, debug unaffected) 2026-09-14 12:29:14 -05:00
avi
d9f3d0b5c4 Android: StrictMode (debug) + off-main-thread IO — root resolver, file import, and DateFormat moved off composition/main; details label preformatted 2026-09-14 12:29:14 -05:00
avi
8c510f4ab9 Local-only app: remove network, uploads, providers, backend, desktop
Phone is now a pure on-device recorder: no accounts, no servers, no
background uploads (INTERNET permission gone). File sync is the user's
own tooling; the library adopts externally added files.

Android:
- Delete provider package (Nextcloud, custom SHONAR, sync-folder,
  registry, auth, TOFU/TLS), sync stack (SyncWorker/Drain/Slots,
  MigrationRunner), provider/storage/folder UI, AI-via-server details.
- Home shows a fixed 'on this phone' library; details keep playback,
  rename, file info. Settings lose Network/provider/HTTP-logging.
- Library root is the stored folder or Music/Recordings; import is
  double-scan proof (mutex + unique filePath index, migration v3->v4
  dedupes by path) with regression tests.
- Drop okhttp/work/security-crypto/media deps; delete their tests.

Repo: backend/, desktop/, worker/, deploy/, shared/, server scripts
and docs removed; README rewritten; CI keeps the android job only.
2026-09-14 11:56:20 -05:00
avi
a5201c1f2f Android: lock ongoing-recording notification to custom card + regression test
The working tree had reverted RecordingNotificationHelper to plain
system MediaStyle, which renders the small 'SHONAR - SHONAR' text row
with truncated filename and no big timer. Restore the user-verified
custom RemoteViews card (mic + Recording + filename + big timer, X /
pause / check pills, same view collapsed and expanded) and drop the
non-compiling MediaSession dead code from RecordingService
(unresolved activeNotifId / mediaSession / alertOnce references).

Add RecordingNotificationLayoutTest to fail the build if MediaStyle
is reintroduced or the timer/pills leave the layout.
2026-09-14 11:14:53 -05:00
avi
67b1eb65c7 Android WIP: recording notification layout + service updates (in-flight from prior session) 2026-09-14 10:37:09 -05:00
avi
e3c4c06d23 Desktop: reprocess path mirrors live progress onto library rows 2026-09-14 09:41:13 -05:00
avi
6824d9fbf1 M9: search endpoint + list filters, exports (audio/txt/md/zip), retention sweep 2026-09-14 08:51:24 -05:00
avi
b121d1a2e3 provider: reprocess(recordingId, job, model) against /reprocess
Server detail message surfaces through ProviderError.Transient so 409
reasons ('already running', 'transcribe first') reach the UI.
2026-09-13 20:36:54 -05:00
avi
8873c759e1 M8 + T1: detail screen with transcript/summary editing; transcription model registry
Backend:
- PUT /recordings/{id}/transcript and /summary — user-edited, versioned;
  the pipeline never clobbers user versions
- Model registry (tiny/base/small/medium/large-v3), global default via
  PUT /models/default (future rows only), per-recording override on
  finalize/upload-session (finalize wins), GET /api/v1/models
- processing_jobs gain stage/progress; faster-whisper model cache +
  fail-fast on unavailable models; migration m8models000001
- 10 model tests + 5 transcript-edit tests (backend suite 64 green, ruff clean)

Android:
- detail/{id} route: transcript/summary/status tabs, tap-to-seek, speed
  control, edit dialogs, title rename via CustomShonarProvider AI methods
- pure AiContent parsing/sync mapping; honest empty states for
  local-only/unsynced rows; 19 new unit tests (Android suite green)
2026-09-12 22:33:16 -05:00
avi
aad7cad78d P3-M7: providers, background sync, AI pipeline
P3 CustomShonarProvider (auth, chunked uploads, secure token store).
P4 NextcloudProvider (login flow v2, DAV, chunking) + FolderSyncProvider
(Syncthing-style). P5 TOFU pinning + redacting logger + leak tests.
P6a generic hosted setup (Start9/Umbrel URL + auto-detect). P7 provider
switching (Room v2 slot, Storage screen, foreground migrator). M5
WorkManager drain (Room v3 backoff). M7 AI pipeline (4 adapters, arq
worker, transcript/summary/jobs endpoints). Docs updated throughout.
2026-09-12 11:29:55 -05:00
avi
b48f77d6e3 Settings: remove dead 'SHONAR server URL' field; mark sync prefs as pending provider
- server_url setting deleted (no code referenced it; it predated the
  provider pivot and misled users into pasting Nextcloud URLs into a
  field nothing reads). The provider-selection flow (P2) replaces it.
- Wi-Fi-only / charging-only uploads now carry a description stating
  they apply once a sync provider is connected.

Verified: 47 unit tests green, APK builds, on-device (Pixel 8 Pro)
Settings > Network shows no SHONAR server URL field and the new
descriptions render.
2026-09-08 19:05:44 -05:00
avi
b4322b0697 P1: provider module — ShonarProvider contract, ServerUrl validation, LocalOnlyProvider
New package com.shonar.provider (docs/server-providers.md §1-2):
- ShonarProvider: single contract between app and any server (probe,
  connect/reconnect/disconnect w/ server-side revoke, deleteAccountAndData,
  upload/download/delete, sidecars, storageLocationSummary, authState flow).
  Documented contract rules: cancel-safe, idempotent per draft id,
  immutable originals, secret-free errors.
- ProviderTypes: ProviderDescriptor+Capability, sealed ProviderCredential
  (OAuthTokens / AppPassword / None — toString() redacts secrets),
  ProbeResult incl. TlsFailure(fingerprint) for explicit TOFU flow and
  ServicesFound for Start9/Umbrel platform probing, AuthState lifecycle,
  RemoteRef (opaque keys, never local paths), SidecarKind, StorageLocation,
  sealed ProviderError.
- ServerUrl.parse: https everywhere; http only for RFC1918/loopback/.local;
  rejects userinfo, traversal, non-http schemes, blanks. isPrivateHost with
  exact range tests (172.16-31 boundary covered).
- LocalOnlyProvider: first-class no-network provider (imports no HTTP lib),
  copy-based upload w/ progress, sidecar dir layout, sha256 etags, account
  deletion wipes root.
- ProviderRegistry: id->factory, active selection never touches local data;
  Nextcloud wins default once registered (P4).

Tests (47 Android unit tests green total, was 19):
- ProviderContractTest: shared suite every provider must pass — roundtrip
  byte-identity, monotonic progress ending 1.0, idempotent re-upload,
  delete removes audio+sidecars, sidecar overwrite, summary counts,
  leak-check (credential strings never appear in error messages).
- LocalOnlyProviderContractTest runs the contract + local specifics
  (probe=Incompatible, credentials rejected, deleteAccount wipes root).
- ServerUrlTest + ProviderRegistryTest: validation matrix and registry rules.

Verified on Pixel 8 Pro: APK installs, app launches, consent dialog
renders, 0 crash-log entries.
2026-09-08 18:38:21 -05:00
avi
4eab1f11cf Product pivot: defer Home Assistant; provider-based server architecture (Nextcloud default)
ISOLATE (nothing deleted):
- moved HA module (ha/, ui/devices/, HA client tests), e2e scripts, and HA
  docs under deferred/home-assistant/ with a README explaining status + how
  to revive; complete snapshot preserved on branch deferred/home-assistant

REMOVE FROM ACTIVE PRODUCT:
- HomeScreen: Devices card + route gone; MainActivity nav updated
- ShonarApplication: haRepository removed
- BuiltInSettings: Home Assistant category/settings removed from defaults
- SettingsManagerTest: secret tests rewritten around a user-created
  SECRET-type setting (no built-in secret ships)
- Manifest + URL-validation test fixture wording neutralized
- README/ROADMAP: HA marked deferred with pointer to preserved branch

ADD (design, per product direction):
- docs/server-providers.md: ShonarProvider interface, Room data model,
  auth ladder (OIDC/PKCE -> Nextcloud login-flow-v2 -> token paste),
  sync strategy, provider-selection UX (Nextcloud default; Start9/Umbrel
  as platform-probe + explicit service binding, never universal APIs;
  custom SHONAR server; local-only), TLS TOFU pinning policy,
  no-secret-logging rules, provider contract test strategy, phased plan P0-P7

VERIFY: 19 Android unit tests green, APK builds, on-device launch OK
(consent dialog renders; no Devices entry). Backend unchanged (26 tests).
2026-09-08 18:19:57 -05:00
avi
4d5402946d Android: app shell + Custom Settings engine + Home Assistant integration
Generic settings architecture (data-driven, no per-setting UI code):
- SettingDefinition (id/name/description/category/type/default/min/max/
  choices/editable/sensitive/requiresRestart/visibleIf) x 8 types:
  boolean/string/number/select/multi-select/color/url/secret
- SettingsManager: validation, reset, custom CRUD, export/import
  (all-or-nothing with per-key rejection reasons; custom definitions
  travel in the export), secrets routed to EncryptedSharedPreferences
  (AndroidKeyStore master key) and excluded from export by default
- Settings screen renders controls from the type; search; add/edit/delete
  dialogs for custom settings; import/export dialogs
- Built-ins: General / Home Assistant / Appearance / Network / Advanced

Home Assistant integration (official REST + WebSocket APIs only):
- HomeAssistantClient: GET /api/, /api/states, /api/states/{id},
  POST /api/services/{domain}/{service}, WS /api/websocket
  (auth -> subscribe_events state_changed) with exponential-backoff
  reconnect; safe HaError types that never contain the token
- HaRepository: single service layer between UI and client; optimistic
  toggles reconciled by WS; poll fallback from refresh-interval setting
- Devices screen: discovery list, search, live states, toggles
  (light/switch/fan/input_boolean/humidifier)
- Cleartext permitted for LAN http:// URLs (same stance as the official
  HA companion app; TLS verification untouched); consent dialog on first
  launch; Record button present but inert until M4 (honest label)
- Tests: 29 unit tests (19 settings incl. secret routing, import
  validation, persistence; 10 HA via MockWebServer incl. 401 handling,
  unreachable, WS handshake + event + auth_invalid). All green.

Docs: docs/home-assistant.md (install, token creation, storage,
troubleshooting, endpoint table); README + ROADMAP updated honestly
(live e2e against a real HA server still in progress).
2026-09-08 16:25:25 -05:00