Release GPS2Audio v2.7.38 Samsung audio fix
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# GPS2Audio v2.7.38 — Handoff (Audio Regression Fix)
|
||||
|
||||
**Date:** 2026-06-22
|
||||
**Base:** v2.7.37 (versionCode 143) → **v2.7.38 (versionCode 144)**
|
||||
**Source:** `/home/user/workspace/GPS2Audio_v2737_from_gitea/src`
|
||||
**Priority honored:** Audio reliability over preserving the silent gap. The gap is kept, but made strictly safe.
|
||||
|
||||
---
|
||||
|
||||
## Suspected root cause
|
||||
|
||||
The only functional change in v2.7.37 was a hidden ~1.7 s silent gap between manual sequence items, implemented as a delayed coroutine (`manualGapJob`) launched inside the `manualPlayer.play(...)` natural-completion callback. The gap itself is correctly scoped to post-completion auto-advance. The regression — "audio broken, especially on Samsung tablets" — comes from **two unsafe state interactions introduced/left open by that delayed job, plus a pre-existing silent-failure path that the gap made far more visible**:
|
||||
|
||||
1. **Stale auto-advance across tour switch / tour delete.**
|
||||
`selectTour()` deliberately preserves running playback (v2.5.30) and did **not** cancel a pending gap job. `deleteTour()` stopped the player but also did **not** cancel the gap job. So a gap job scheduled in tour A could fire ~1.7 s later after the user switched to / deleted tour A, calling `playFromCurrentPlayable(autoAdvance=true)` and setting `_manualPlaying=true` against stale `_currentPlayable` — leaving the UI in a "playing" state with the wrong audio or no audio.
|
||||
|
||||
2. **Gap job did not re-validate user intent before firing.**
|
||||
The delayed job only re-checked PTT and whether the next item still exists. It did **not** check whether the user had paused/stopped or switched to single-clip mode during the silence, so it could resurrect playback the user had just stopped.
|
||||
|
||||
3. **Silent load/start failure left state as "playing".**
|
||||
`ManualAudioPlayer.loadAndPlay()` uses raw `MediaPlayer` with synchronous `prepare()`/`start()` and **no error feedback to the caller**. On some devices (notably Samsung tablets) `prepare()`/`start()` on `content://` SAF URIs, or an async `MediaPlayer` error, fails — the player released silently while the ViewModel had already set `_manualPlaying=true`. Result: UI shows "playing", no sound, no error. The v2.7.37 gap (which chains auto-advance starts) made this dead-end far more reachable across a sequence.
|
||||
|
||||
---
|
||||
|
||||
## Exact fixes
|
||||
|
||||
### `app/src/main/kotlin/de/waypointaudio/viewmodel/WaypointViewModel.kt`
|
||||
- **`playFromCurrentPlayable(autoAdvance)` — gap job hardened.** Capture `gapTour = _selectedTour.value` when scheduling. After the `delay`, before advancing, the job now aborts if:
|
||||
- the selected tour changed during the silence (`_selectedTour.value != gapTour`) — no start in the wrong tour;
|
||||
- the user is no longer playing or has entered single mode (`!_manualPlaying.value || manualPlayer.isSingleMode`) — no resurrection after pause/stop/preview.
|
||||
PTT and "next item removed" checks unchanged. This makes the gap **strictly post-completion auto-advance within the same tour**.
|
||||
- **`playFromCurrentPlayable` — `onError` wired.** `manualPlayer.play(...)` is now called with an `onError` callback that cancels any gap, resets `_manualPlaying`/`_manualCurrentName`/`_currentPlayable`, stops the ticker, calls `musicManager.afterWaypointAudio()`, and surfaces `_errorMessage` ("Audio konnte nicht abgespielt werden. Bitte erneut versuchen."). No more silent "playing-with-no-audio".
|
||||
- **`selectTour()`** — cancels a pending gap job on an actual tour change (`if (safeTarget != _selectedTour.value) cancelManualGap()`), without stopping active playback (persistence preserved).
|
||||
- **`deleteTour()`** — calls `cancelManualGap()` alongside the existing `manualPlayer.stop()` reset when the deleted tour is selected.
|
||||
|
||||
### `app/src/main/kotlin/de/waypointaudio/service/ManualAudioPlayer.kt`
|
||||
- **`play(...)`** gains an optional `onError: () -> Unit = {}` (placed before `onCompletion` so the trailing-lambda completion call sites stay valid). Fires `onError()` on blank URI.
|
||||
- **`loadAndPlay(...)`** gains optional `onError`; fires it both on the synchronous `runCatching{}.onFailure` (prepare/start failure) and inside `setOnErrorListener` (async MediaPlayer error), after releasing resources.
|
||||
- **`startSingleLoad(...)`** updated to named `onCompletion =` argument (behavior identical; single-clip preview/GPS unaffected since `onError` defaults to no-op).
|
||||
|
||||
### `app/build.gradle.kts`
|
||||
- `versionCode 143 → 144`, `versionName "2.7.37" → "2.7.38"`, with a v2.7.38 changelog comment block.
|
||||
|
||||
**First manual playback is unchanged and still immediate** — the gap only exists in the auto-advance callback after a clip ends, never on the initial `startPlayableAt` → `playFromCurrentPlayable` path. `cancelManualGap()` is still called at the top of `startPlayableAt`, so Play/Resume/Next/Prev/Skip cannot be suppressed by a pending gap.
|
||||
|
||||
---
|
||||
|
||||
## Playback paths reviewed
|
||||
|
||||
| Path | Verdict |
|
||||
|---|---|
|
||||
| Manual sequence play/pause/resume/next/prev/stop | **Fixed/hardened** (above). `cancelManualGap()` confirmed on every control path. |
|
||||
| Auto-advance silent gap (v2.7.37) | **Kept but made strictly safe** (same-tour, still-playing, post-completion only). |
|
||||
| Single clip preview (`manualPlaySingleClip` / `playSingle` / `startSingleLoad`) | Reviewed — unchanged behavior; `onError` defaults to no-op. No change needed. |
|
||||
| GPS-triggered playback (`service/AudioPlayer.kt`) | Separate class, not touched by these edits. No change needed. |
|
||||
| PTT suppression / interruption coordinator | Reviewed — gap job and play paths still honor `pttManager.pttActive`; coordinator pause/resume unchanged. |
|
||||
| Atmo / background music (`BackgroundMusicPlayer`, `musicManager`) | Not touched. `afterWaypointAudio()`/`beforeWaypointAudio()` calls preserved. |
|
||||
| Background manual playback (`ManualPlaybackService`) | Not touched; start/stop/meta lifecycle unchanged. |
|
||||
| Tour switch / delete playback persistence (v2.5.30) | Preserved — active playback still survives a plain tour switch; only the stale *pending gap* is cancelled. |
|
||||
| Player auto-open (v2.7.32) | Not touched. |
|
||||
|
||||
---
|
||||
|
||||
## Build / test results
|
||||
|
||||
- **Signed APK: NOT BUILT — toolchain unavailable in this environment.**
|
||||
- No Android SDK (`/home/user/tools/android-sdk` missing).
|
||||
- No Gradle (`gradlew` execs `/home/user/tools/gradle/bin/gradle`, which does not exist; `/home/user/tools/` is absent).
|
||||
- No keystore (`keystore.properties` points to `/home/user/workspace/GPS2Audio_release_keystore_v2.jks`, which is missing).
|
||||
- The Gradle release signing config itself is intact in `app/build.gradle.kts` (reads `keystore.properties` or `GPS2AUDIO_RELEASE_*` env vars), so a signed universal release APK **will** build on a properly provisioned machine: `./gradlew :app:assembleRelease` with the keystore + SDK present.
|
||||
- **Compilation:** could not run (no SDK/Gradle). Changes were made minimal and verified by manual review for Kotlin correctness:
|
||||
- `play(...)` parameter order changed so the existing trailing-lambda completion call site still binds to `onCompletion`; the new ViewModel call passes `onError = { ... }` (named) + trailing `onCompletion` lambda.
|
||||
- All three `loadAndPlay(...)` call sites match the updated 4-arg signature.
|
||||
- **Recommended manual QA on a Samsung tablet:** (1) play a tour clip and let it auto-advance — verify ~1.7 s gap then next clip; (2) during the gap, switch tours — verify no audio starts in the new tour and no stuck "playing"; (3) during the gap, delete the tour — verify clean stop; (4) during the gap, tap pause/stop — verify it stays stopped; (5) force a bad/unreadable clip — verify error toast appears and state resets instead of silent "playing"; (6) first manual play — verify it starts immediately with no delay.
|
||||
|
||||
---
|
||||
|
||||
## Artifacts
|
||||
|
||||
- **Clean source ZIP:** `/home/user/workspace/GPS2Audio_v2738_artifacts/GPS2Audio_v2.7.38_src.zip`
|
||||
- 136 files. Excludes `.gradle`, `.idea`, `.kotlin`, all `build/` dirs, APK/AAB/JKS/keystore binaries, `.class`/`.dex`, and `.git`.
|
||||
- **Also excludes `keystore.properties` and `local.properties`** — `keystore.properties` contained plaintext signing passwords (do not redistribute); `local.properties` is machine-specific. Recreate both from the build machine's secrets when building.
|
||||
- **This handoff:** `/home/user/workspace/GPS2Audio_v2738_artifacts/HANDOFF_v2.7.38.md`
|
||||
|
||||
## Constraints honored
|
||||
Preserved v2.7.32 (tour ordering + player auto-open), v2.7.33 (Begleitmusik UI), v2.7.34 (waypoint edit UI), v2.7.35 (home UI), v2.7.36 (backup export IO fix), v2.7.37 gap (kept, made safe), and Explorer/MapLibre/routing/POI/settings/license/PTT/Atmo/multiclip/background playback. No refactors, no data-model changes.
|
||||
Binary file not shown.
Reference in New Issue
Block a user