Fix AAOS steering-wheel skip buttons after Android 14 update#5328
Fix AAOS steering-wheel skip buttons after Android 14 update#5328joashrajin wants to merge 4 commits into
Conversation
After the Media3 migration, the player no longer advertised COMMAND_SEEK_TO_NEXT / COMMAND_SEEK_TO_PREVIOUS, and the onMediaButtonEvent handler did not recognise KEYCODE_MEDIA_FAST_FORWARD, KEYCODE_MEDIA_REWIND, or the Android 14 AAOS fallback KEYCODE_MEDIA_STOP that GM's car-media service sends when no skip action is reachable. As a result, steering-wheel skip buttons on Chevrolet Equinox EV (and likely other AAOS vehicles after the Android 14 vehicle-OS update) did nothing. Expose SEEK_TO_NEXT / SEEK_TO_PREVIOUS on both TRANSPORT_PLAYER_COMMANDS and PocketCastsForwardingPlayer.getAvailableCommands() so AAOS routes the steering-wheel skip buttons through seekToNext()/seekToPrevious() (already wired to PlaybackManager.skipForwardSuspend / skipBackwardSuspend via onSkipForward / onSkipBack). Add explicit handlers for KEYCODE_MEDIA_FAST_FORWARD / KEYCODE_MEDIA_REWIND as a defence-in-depth for controllers that still dispatch raw key events. On automotive, bypass the headphone multi-tap queue for KEYCODE_MEDIA_NEXT / KEYCODE_MEDIA_PREVIOUS so a single press of the steering-wheel skip button triggers an immediate skip rather than waiting on the 250 ms multi-tap timeout.
Generated by 🚫 Danger |
There was a problem hiding this comment.
Pull request overview
Fixes Android Automotive OS (AAOS) steering-wheel skip controls becoming unresponsive after the Media3 migration / Android 14 AAOS rollout by ensuring the Media3 session advertises skip support and by handling additional media keycodes and automotive-specific behavior in the media-button event path.
Changes:
- Advertise
COMMAND_SEEK_TO_NEXT/COMMAND_SEEK_TO_PREVIOUSfrom both the forwarding player and session callback so AAOS/controllers route skip via Media3 commands (instead of falling back to STOP). - Handle
KEYCODE_MEDIA_FAST_FORWARD/KEYCODE_MEDIA_REWINDas direct skip forward/back inonMediaButtonEvent. - On AAOS, bypass the multi-tap queue for
KEYCODE_MEDIA_NEXT/KEYCODE_MEDIA_PREVIOUSto avoid the 250ms delay and ensure immediate response.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/playback/PocketCastsForwardingPlayer.kt | Advertises seek-to-next/previous commands so external controllers can invoke skip via Media3. |
| modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/playback/Media3SessionCallback.kt | Adds skip command exposure, handles FAST_FORWARD/REWIND keycodes, and introduces AAOS-specific bypass of multi-tap logic for NEXT/PREVIOUS. |
| modules/services/repositories/src/test/java/au/com/shiftyjelly/pocketcasts/repositories/playback/PocketCastsForwardingPlayerTest.kt | Updates expectations to match new command exposure for both initial and swapped players. |
| modules/services/repositories/src/test/java/au/com/shiftyjelly/pocketcasts/repositories/playback/Media3SessionCallbackTest.kt | Adds coverage for FAST_FORWARD/REWIND handling and the AAOS NEXT/PREVIOUS immediate-skip behavior, and updates command exposure assertions. |
The PCDROID-560 changes ended up with four near-identical
`scope.launch { try { playbackManager.skipXxxSuspend(...) } catch { Timber.e(...) } }`
blocks across the SKIP_FORWARD/FAST_FORWARD, SKIP_BACKWARD/REWIND, and
automotive NEXT/PREVIOUS handlers. Collapse them into two private helpers
so the four call sites are one-liners and future changes to logging or
error handling stay in one place.
No behaviour change.
| // steering-wheel buttons) route through seekToNext()/seekToPrevious() | ||
| // instead of falling back to STOP. See PCDROID-560. | ||
| Player.COMMAND_SEEK_TO_NEXT, | ||
| Player.COMMAND_SEEK_TO_PREVIOUS, |
There was a problem hiding this comment.
Good catch, thanks for the screenshot. Fixed in 2708c28 — dropped the COMMAND_SEEK_TO_NEXT / COMMAND_SEEK_TO_PREVIOUS advertisement entirely (both in PocketCastsForwardingPlayer.getAvailableCommands() and TRANSPORT_PLAYER_COMMANDS), and restored the swapPlayer excludes seek to next and previous test to its original assertion.
The reported PCDROID-560 bug is keycode-based — the user's log showed Media3 media button event: keyCode=86 (KEYCODE_MEDIA_STOP), so AAOS dispatched the steering-wheel press as a key event. The keycode handlers in this PR (FAST_FORWARD / REWIND + automotive NEXT / PREVIOUS bypass) are what actually fix it; the command advertisement was belt-and-suspenders for any AAOS implementation that might route via Media3 commands instead of key events. If we ever see one that needs the command route, we can re-add it scoped per-controller via ConnectionResult in onConnect so Wear OS isn't affected.
There was a problem hiding this comment.
Is this better @geekygecko or do you have any other suggestions?
| return true | ||
| } | ||
| } | ||
| } |
@geekygecko reported on PR 5328 that advertising these commands unconditionally caused Wear OS to render duplicate previous-track / skip-back controls alongside the existing skip buttons. The reported AAOS bug (PCDROID-560) is keycode-based — the user's log showed `Media3 media button event: keyCode=86` (KEYCODE_MEDIA_STOP), meaning AAOS dispatched the steering-wheel press as a key event after its skip-command lookup failed. The keycode handlers in this PR (FAST_FORWARD/REWIND + automotive NEXT/PREVIOUS bypass) are what actually resolve the bug; the command advertisement was belt-and-suspenders for any AAOS implementation that might route via Media3 commands instead of key events. Drop the advertisement to fix the Wear OS regression. If we ever see an AAOS implementation that needs the command route, we can re-add it scoped per-controller via ConnectionResult in onConnect. Restores the `swapPlayer excludes seek to next and previous` test to its original assertion.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
modules/services/repositories/src/test/java/au/com/shiftyjelly/pocketcasts/repositories/playback/PocketCastsForwardingPlayerTest.kt:170
- This test name now says skip-to-next/previous are exposed, but the assertions below still expect
COMMAND_SEEK_TO_NEXT/COMMAND_SEEK_TO_PREVIOUSto be absent. Either update the assertions (and productionPocketCastsForwardingPlayer.getAvailableCommands()) to include those commands, or revert the test name to match the current behavior.
fun `available commands expose core controls and skip-to-next-previous`() {
val commands = forwardingPlayer.availableCommands
assertTrue(commands.contains(Player.COMMAND_PLAY_PAUSE))
assertTrue(commands.contains(Player.COMMAND_SET_MEDIA_ITEM))
| // PiP skip buttons and the FAST_FORWARD/REWIND media keys (used by some | ||
| // AAOS steering-wheel implementations) always skip forward/back, bypassing | ||
| // headphone control settings. See PCDROID-560. | ||
| KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, | ||
| KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, | ||
| -> { |
There was a problem hiding this comment.
The PR description was stale — updated. The advertisement was removed deliberately in 2708c28 after @geekygecko flagged that exposing those commands unconditionally caused Wear OS to render duplicate skip controls (screenshot).
You're right that AAOS controllers will continue to send fallback keycodes — that's the intended outcome now. The reported PCDROID-560 case is keycode-based (Media3 media button event: keyCode=86), and the FAST_FORWARD/REWIND + automotive NEXT/PREVIOUS handlers in this PR catch every fallback path. If a future AAOS implementation ever routes only via Media3 commands, we'd re-add the advertisement scoped per-controller via ConnectionResult in onConnect so Wear OS stays clean.

Description
Fixes https://linear.app/a8c/issue/PCDROID-560/aaos-steering-wheel-skip-forwardback-buttons-unresponsive-in-pocket
After the Media3 migration, the
onMediaButtonEventhandler didn't recogniseKEYCODE_MEDIA_FAST_FORWARD,KEYCODE_MEDIA_REWIND, or the fallbackKEYCODE_MEDIA_STOPthat GM's car-media service sends when no skip action is reachable, and on AAOS the headphone multi-tap queue swallowedKEYCODE_MEDIA_NEXT/KEYCODE_MEDIA_PREVIOUSfor 250 ms before resolving them. After the Android 14 vehicle-OS rollout on Chevrolet Equinox EV (and likely other AAOS platforms), the steering-wheel skip-forward / skip-back buttons became completely unresponsive — the user reportedMedia3 media button event: keyCode=86(KEYCODE_MEDIA_STOP) in their log when pressing skip.Changes
Media3SessionCallback.onMediaButtonEvent— handleKEYCODE_MEDIA_FAST_FORWARDandKEYCODE_MEDIA_REWINDas direct skip-forward / skip-back, alongside the existingKEYCODE_MEDIA_SKIP_FORWARD/KEYCODE_MEDIA_SKIP_BACKWARDcases.Media3SessionCallback.onMediaButtonEvent— on Android Automotive OS, bypass the headphone multi-tap queue forKEYCODE_MEDIA_NEXTandKEYCODE_MEDIA_PREVIOUS. The 250 ms multi-tap window is correct for Pixel Buds but wrong for a steering-wheel button, which fires once per press.Media3SessionCallback— extractedlaunchSkipForward()/launchSkipBackward()private helpers so the four skip call sites (SKIP_FORWARD / FAST_FORWARD / automotive NEXT / automotive PREVIOUS, and their backward equivalents) share one implementation.Why not advertise
COMMAND_SEEK_TO_NEXT/COMMAND_SEEK_TO_PREVIOUS?An earlier revision of this PR also added those commands to
TRANSPORT_PLAYER_COMMANDSandPocketCastsForwardingPlayer.getAvailableCommands()as belt-and-suspenders for controllers that route skip via Media3 commands instead of key events. @geekygecko pointed out that advertising them unconditionally caused Wear OS to render a duplicate "previous track / next track" pair alongside the existing skip-forward / skip-back buttons (screenshot). The reported PCDROID-560 case is purely keycode-based, so the keycode handlers above resolve the bug on their own. If a future AAOS implementation needs the command route, we can re-add it scoped per-controller viaConnectionResultinonConnectso Wear OS isn't affected.Testing Instructions
Automated tests
./gradlew :modules:services:repositories:testDebugUnitTest --tests "au.com.shiftyjelly.pocketcasts.repositories.playback.Media3SessionCallbackTest"./gradlew :modules:services:repositories:testDebugUnitTest --tests "au.com.shiftyjelly.pocketcasts.repositories.playback.PocketCastsForwardingPlayerTest"KEYCODE_MEDIA_FAST_FORWARD calls skipForwardSuspend directlyKEYCODE_MEDIA_REWIND calls skipBackwardSuspend directlyon automotive, KEYCODE_MEDIA_NEXT skips forward immediatelyon automotive, KEYCODE_MEDIA_PREVIOUS skips backward immediatelySEEK_TO_NEXT/SEEK_TO_PREVIOUSare intentionally not advertised.Manual — AAOS emulator
./gradlew :automotive:assembleDebug.KEYCODE_MEDIA_NEXTkey event viaadb shell input keyevent 87(and88for previous,90for fast-forward,89for rewind).Media3: stop → pauselog line is emitted.Manual — mobile regression check
debugvariant.headphoneControlsNextAction/headphoneControlsPreviousActionafter the 250 ms window — the automotive bypass only applies whenUtil.isAutomotive(context)is true.Manual — Wear OS regression check
Checklist
./gradlew spotlessApplyto automatically apply formatting/linting)modules/services/localization/src/main/res/values/strings.xml(no string changes)Note
Tests were not executed locally — the current
mainbranch pulls in acrashlogging:6.0.8dependency compiled to Java 21 class files, butlibs.versions.tomlstill pinsjava = "17". Without a local JDK 21 install,:modules:services:crashlogging:compileDebugJavaWithJavacfails before any test code runs. Pre-existing issue, not caused by this PR — relying on CI for verification.