Skip to content

[fix] Android: stopping a meeting can no longer kill the process, and a failed one releases the microphone service - #355

Merged
YJack0000 merged 1 commit into
mainfrom
fix/android-meeting-stop
Sep 11, 2026
Merged

YJack0000 merged 1 commit into
mainfrom
fix/android-meeting-stop

Conversation

@YJack0000

Copy link
Copy Markdown
Contributor

What this changes

Closes the remaining process-death paths in the Android meeting flow found by a static audit after the Play rejection (#354), fixes the release workflow so the Play upload succeeds while the app has a rejected change outstanding, and bumps Android to 0.1.3 (4).

Why

Play's rejection of 0.1.1 (2) ("app opens but keeps stopping") was traced to the DataStore/R8 crash fixed in #354. Auditing the rest of the app for the same class of failure turned up five more, all reachable by a reviewer who records a meeting:

Defect Effect Fix
MicCapture.start() reset the stop flag at collection time Stop tapped while still Connecting was undone: mic kept recording, stop() timed out, finished the encoder, next chunk threw append() after finish() → process death MicCapture is one-shot; MeetingSession skips the Recording transition when a stop already won
No CoroutineExceptionHandler anywhere Any exception outside the narrow catches (MediaCodec state errors, DataStore I/O, task swiped away racing a buffered chunk) killed the process App/meeting/import scopes log; both capture loops map the unexpected to a Failed state
MeetingService could stop without ever calling startForeground() after startForegroundService() ForegroundServiceDidNotStartInTimeException on the stop action reaching a fresh instance, on a start racing clear(), on the demo action in release ensureForeground() before every stop-capable path
A failed capture never stopped the service "Recording a meeting" notification and the microphone FGS stayed until the app was killed Service observes session.state and stops on Failed; clear() stops a leftover instance
Library LazyColumn keyed pending and cloud rows on the same id Duplicate-key IllegalArgumentException on open after a drained upload Namespaced keys

Release workflow: the android-v0.1.2 run failed at Upload to Play with Changes cannot be sent for review automatically. Please set the query parameter changesNotSentForReview to true. Play refuses an auto-submitting edit while the app has a rejected/unreviewed change. The upload step now passes changesNotSentForReview: true; sending for review is a click in Play Console (documented in RELEASING.md).

Part of #244.

How it was verified

Release build (assembleRelease, minified) on an Android 14 / API 34 Play emulator:

  • Upgrade install over 0.1.2 (3), cold start with the stored token
  • Record → Stop → "Uploaded", recording present in GET /recordings
  • Stop from the notification action while backgrounded → service and notification gone
  • Task swiped away mid-recording → recording survives (by design, [fix] the recording survives leaving the foreground, and one tap means one recording #272), no crash
  • Record then Stop back-to-back ×3 → no crash, no leftover service
  • Import (m4a via SAF) → transcribed and uploaded
  • ./gradlew :parleykit:test :app:testDebugUnitTest pass
  • bunx tsc --noEmit / bunx vitest run — not applicable, no desktop code touched

🤖 Generated with Claude Code

… a failed one releases the microphone service

A static pass over the Android app after the Play rejection (see #354) found
several ways the meeting flow could take the process down, all of them on the
path from a recording to its end:

- `MicCapture.start()` reset the stop flag at collection time, so a Stop tapped
  while the session was still connecting was undone: the read loop kept going,
  `stop()` gave up waiting after 5 s and finished the encoder, and the next
  chunk hit `append() after finish()` — an IllegalStateException nothing caught,
  on a scope with no handler. The microphone also kept recording after Stop.
  The instance is now one-shot: a stop is never undone, and the session skips
  the "recording" transition when a stop already won.
- Nothing in the app installed a CoroutineExceptionHandler, so any exception the
  narrow catches did not cover (MediaCodec state errors, DataStore I/O, a bad
  header on reconnect, the task-swiped-away teardown racing a buffered chunk)
  reached the default handler and killed the process. The app, meeting and
  import scopes now log instead, and both capture loops map anything unexpected
  to a Failed state the user can retry from.
- `MeetingService` had three paths that could stop without ever having called
  `startForeground()` after a `startForegroundService()` — which Android
  answers with ForegroundServiceDidNotStartInTimeException. Every such path now
  goes through `ensureForeground()` first.
- A failed capture (mic busy, expired token, no encoder) left the microphone
  foreground service and its "Recording a meeting" notification running until
  the app was killed. The service now watches the session and stops on Failed;
  `clear()` stops a leftover instance too.
- The library LazyColumn keyed pending uploads and cloud recordings on the same
  id; a recording drained between the two reads appeared in both lists and
  Compose threw on the duplicate key. Keys are namespaced.

The release workflow now commits the Play edit with `changesNotSentForReview`:
while the app has a rejected change outstanding, Play refuses an edit that
would auto-submit, which is how the android-v0.1.2 upload failed. Sending the
release for review is a click in Play Console, documented in RELEASING.md.

versionCode 4 / versionName 0.1.3. Verified on the release build (Android 14
emulator): upgrade install over 0.1.2, cold start, record → Stop → upload,
Stop from the notification while backgrounded, task swiped away mid-recording,
file import — no crash, no leftover service or notification.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@YJack0000
YJack0000 merged commit d10598e into main Sep 11, 2026
4 checks passed
@github-actions

Copy link
Copy Markdown

✅ SonarQube Quality Gate passed — pathorsAI_parley

0 open issues on this PR.

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