Repository navigation
feat(meeting): keep every recording on the phone for 90 days - #58
Conversation
…nnect - MicLevelMonitor: fail-closed Mic signal. Green only with a non-zero frame in the last 2 s; red after 5 s of all-zero audio or 5 s with no frames, and at once when Android reports the capture silenced (AudioRecordingConfiguration, API 29+). Re-judged on a 200 ms clock so a stalled capture loop cannot stay green. - Live mic meter in the app and a level bar in the notification; the notification leads with the worst signal. One alert per incident: the mic alert clears only after 3 s of continuous sound. - TailscaleLink: when no VPN is up, ask Tailscale to connect through its exported CONNECT_VPN receiver; after 20 s without a VPN, alert with a tap-to-open action. The network note shows only while delivery is failing. - A sticky restart after an OS kill no longer tries to reopen the mic (Android 14+ refuses background microphone FGS starts); it posts a notification that opens the app, which restarts capture through the normal Start path. - Setup checklist (Always-on VPN, never-sleeping apps, mic access tile) and a banner when notifications are disabled. - Version 0.4.0 (4) with the build time shown.
…e floor - LocalRecorder: every capture is also written on the phone, independent of the live stream. The capture loop hands frames to a bounded queue (never blocks); a per-run writer thread encodes AAC-LC 32 kbps 16 kHz mono and writes raw ADTS frames in 5-minute segments, each with a sidecar written at open (UTC start, run id, sample position). Dropped frames are carried in order and encoded as silence, so positions track capture time. - Retention: older than 90 days is deleted; below 1 GB allocatable, older than 45 days too; never anything newer than 45 days. Runs at each capture start, under storage pressure, and on every app open. Open and future-dated segments are never touched. - Storage floor: below 300 MB the backup pauses (resumes above 400 MB) and the live stream continues. Encoder failures retry after 30 s, permanent after 3. - Fail-closed Local backup signal (green only while bytes reach disk) with a loud alert when it goes off; each closed file is re-read and its valid audio length shown; an "On this phone" summary reads the folder directly. - Version 0.5.0 (5).
- SegmentSync: the recordings folder is the queue (X.aac waiting, X.synced.aac confirmed). Each closed segment, including the short final one at Stop, is uploaded; it becomes synced only when the edge's re-read sha256 and length equal the phone's own. After a 7-day grace (sooner only when storage is low) the phone asks the edge again (/have, re-hashed from disk): yes deletes, no re-queues, no answer keeps. Any doubt keeps the file. - Segment names carry a per-run UUID (the in-memory run counter restarts with the process and could collide); older names are migrated once. - SyncWorker (WorkManager): a kick on every closed file and app open (REPLACE, so a recovered network is used at once) plus an hourly catch-up that survives restarts and reboots; one pass at a time. A file the edge cannot take now does not block the others. A loud alert if anything has waited over a day; the status shows waiting / confirmed / refused / expired-before-sync. - Retention purge shares a folder lock with sync and reports unsynced expiries. Version 0.6.0 (6).
…s as the tailnet - Tap-to-resume restores the persisted capture settings (endpoint, token, model) instead of the build defaults a fresh screen shows; with none saved it refuses to start rather than silently using defaults. - The tailnet check requires a VPN network carrying a Tailscale address (the RFC 6598 /10 or Tailscale's IPv6 /48), so another VPN no longer suppresses auto-connect and the Tailscale alert. - The meter drops to zero once sound is no longer fresh. - Stop clears the network note before publishing the stopped state. - The notifications-off banner also catches the app-level switch and a muted failure channel.
…into feat/meeting-mic-local-backup # Conflicts: # clients/android-mic/app/src/main/java/com/genesis/meetingmic/MainActivity.kt
…eview - Each queued frame carries its capture time; a segment's UTC start is when its first audio was captured, not when the lagging writer encoded it. - Filling dropped audio with silence stops as soon as the writer goes off. - The writer holds its own wake lock in renewed short leases for its whole run, so the drain after Stop stays awake however long the meeting was. - A write error resumes once free space is back above the 300 MB floor; only a full disk waits for the 400 MB margin. - An encoder that fails to configure is released; a missing encoder counts as an encoder failure, not a storage failure. - The storage summary is refreshed again after the final file has closed.
…l-backup # Conflicts: # clients/android-mic/app/build.gradle.kts # clients/android-mic/app/src/main/java/com/genesis/meetingmic/MainActivity.kt # clients/android-mic/app/src/main/java/com/genesis/meetingmic/MicStreamService.kt # clients/android-mic/app/src/main/java/com/genesis/meetingmic/ProblemAlerts.kt # clients/android-mic/app/src/main/res/layout/activity_main.xml # clients/android-mic/app/src/main/res/values/strings.xml
…saved - Audio dropped after the last queued frame is filled with silence at Stop, so the file is as long as the capture. - An encoder that does not finish a segment (no end-of-stream within the bound, or no input slot for it) is an encoder failure, not a finished file. - A failed fsync turns the backup off with an alert instead of being ignored; a failed close reports the last file as not fully saved. - minSdk 30: from Android 11 other apps cannot read this app's external files folder, where recordings are kept.
… encrypted only Merges the updated local-backup base, then: - The sync line refreshes whenever a sync job changes state, so it is current while the screen stays open. - Legacy segment names migrate to a name derived from the old one, so an interrupted migration finishes next time and the sidecar follows its audio; retention runs the migration before sweeping orphan sidecars. - Sync uses only a wss:// (https) address; with a plain ws:// address the files wait on the phone and the status says why. - The upgrade fallback saves the last capture's settings as the sync endpoint, not the form's build defaults.
…e, never overwrites - The local writer is finished when the capture job completes, so the read in flight at Stop is kept; a job cancelled before it started still finishes its writer. - Each new writer waits for the previous one in this process to release its encoder, across service restarts. - A segment is never opened over an existing recording. - A segment that loses its tail at a 5-minute rollover is reported and recording continues with a fresh encoder; only repeated failures stop it. - Retention removes a sidecar only once its audio is really gone.
…nto feat/meeting-mic-sync # Conflicts: # clients/android-mic/app/src/main/java/com/genesis/meetingmic/LocalRecorder.kt
Each capture run registers its endpoint, and every segment records it in a small X.dest file when it opens. Uploads and the later "still held" checks go only to that endpoint, so changing the bridge or token no longer redirects queued recordings. A recording captured over ws:// has no secure destination: it stays on the phone and is reported. Recordings from earlier builds take the endpoint those builds were using, once, and only if they were made before the upgrade. One unreachable bridge no longer holds back uploads to another. The stuck-recording alert now also runs from a job with no network requirement, so a phone offline for days still gets it, and recordings that can never sync raise it too.
…ngs over The destination id is now the bridge address alone. Rotating the token on the same address re-points recordings still waiting to the new token, instead of leaving them refused until they expire. A different address is still a different destination and never receives them.
…line A new token for a known bridge now lifts the day-long refusal delay, so files refused under the old token are tried again on the next pass. The sync headline no longer reads "all recordings" or "nothing waiting" while some recordings have no secure destination and can never sync. The stuck-recording alert is raised or cleared by what is actually stuck, on every pass: it is cleared once nothing is stuck, and a pass that finished no longer clears an alert that still applies.
The sync pass no longer deletes anything. A week after the edge confirms a recording it still asks once whether the edge holds those exact bytes: a yes leaves an X.checked mark, a no puts the file back in the upload queue. Retention now owns every delete. Anything older than 90 days goes. Under storage pressure, copies the edge confirmed may go early (re-confirmed ones first, oldest first), only until there is room again. Every synced copy carries the edge's hash-verified receipt, so nothing is deleted early that the edge never held. An unsynced recording is the only copy and is never deleted early: a full phone turns the local backup off instead (the previous 45-day low-storage floor is removed). Version 0.6.1.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Warning Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee2d73fcf8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| RetentionPolicy.sweep(segments, nowUtcMs, hasRoom) { s -> | ||
| val f = files.getValue(s.id) | ||
| if (!f.exists()) continue // renamed by sync since the listing; judged next time | ||
| // Renamed by sync since the listing (or already gone): judged next time. This is what | ||
| // keeps a copy the edge disowned (renamed back to X.aac) from going as a held one. | ||
| if (!f.exists()) return@sweep |
There was a problem hiding this comment.
Prevent purge from racing a negative edge recheck
When storage is low and /have reports false, runPass receives that answer before acquiring folderLock to rename the recording back to .aac. A concurrent purge can acquire the lock first, see the still-existing .synced.aac here, and delete it as an early-retention candidate; the subsequent rename then finds no file, leaving neither the edge nor the phone with the recording. The existence check only handles a rename that has already completed, so the negative recheck and purge deletion need coordination that covers this window.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 9242801: the pass claims the recording (SegmentSync.verifying) under folderLock before calling /have and releases it only after acting on the answer; purgeDir skips any claimed recording under the same lock. A "no" therefore always finds the file and re-queues it. Every sync call also has a 10-minute overall timeout so a claim cannot be held indefinitely. Tests: aRecordingIsClaimedAgainstPurgeWhileTheEdgeIsAsked, purgeLeavesARecordingAloneWhileItsEdgeCopyIsBeingRechecked.
…ng or the queue A failure to schedule the sync job when a segment closes is logged instead of being read as an encoder failure that paused the local backup. An unreadable sidecar holds back only its own file. After a day that recording uploads without its metadata, so the phone never keeps the only copy because of a broken sidecar.
…/meeting-mic-keep-90d
…-90d # Conflicts: # clients/android-mic/app/build.gradle.kts # clients/android-mic/app/src/main/java/com/genesis/meetingmic/LocalRecorder.kt # clients/android-mic/app/src/main/java/com/genesis/meetingmic/MainActivity.kt # clients/android-mic/app/src/main/java/com/genesis/meetingmic/RetentionPolicy.kt # clients/android-mic/app/src/main/java/com/genesis/meetingmic/SegmentSync.kt # clients/android-mic/app/src/test/java/com/genesis/meetingmic/LocalBackupTest.kt # clients/android-mic/app/src/test/java/com/genesis/meetingmic/SegmentSyncPassTest.kt # clients/android-mic/app/src/test/java/com/genesis/meetingmic/SegmentSyncTest.kt
…-checked While the sync pass asks the edge whether it still holds a recording, a low-storage purge could delete the phone copy just before an answer of "no" put it back in the queue, leaving no copy anywhere. The pass now claims the recording under the folder lock before asking, and purge leaves any claimed recording alone. Every sync call also gets an overall time limit, so a slow edge cannot hold a claim open indefinitely.
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
What changes
The phone keeps every recording for 90 days, even after the edge confirms it holds a copy. This replaces #57's delete-7-days-after-confirmation rule.
/have) whether it still holds those exact bytes:X.checkedmark and keeps the phone copy;RetentionPolicy.sweep, run byLocalRecorder.purgeDir):X.synced.aacexists only after the edge returns a hash-verified receipt, so nothing deleted early was never held by the edge.purgeDir(dir, now, hasRoom), so its rename race is tested.Tests
Shape: about 300 changed lines, one concern.
E2E: after the APK is installed, record a short meeting and confirm the segment becomes
.synced.aacand stays on the phone. A week later, confirm anX.checkedmark appears beside it and the copy is still present.