Skip to content

Fix AAOS steering-wheel skip buttons after Android 14 update#5328

Open
joashrajin wants to merge 4 commits into
mainfrom
fix/pcdroid-560-aaos-steering-wheel-skip
Open

Fix AAOS steering-wheel skip buttons after Android 14 update#5328
joashrajin wants to merge 4 commits into
mainfrom
fix/pcdroid-560-aaos-steering-wheel-skip

Conversation

@joashrajin
Copy link
Copy Markdown
Contributor

@joashrajin joashrajin commented May 22, 2026

Description

Fixes https://linear.app/a8c/issue/PCDROID-560/aaos-steering-wheel-skip-forwardback-buttons-unresponsive-in-pocket

After the Media3 migration, the onMediaButtonEvent handler didn't recognise KEYCODE_MEDIA_FAST_FORWARD, KEYCODE_MEDIA_REWIND, or the fallback KEYCODE_MEDIA_STOP that GM's car-media service sends when no skip action is reachable, and on AAOS the headphone multi-tap queue swallowed KEYCODE_MEDIA_NEXT / KEYCODE_MEDIA_PREVIOUS for 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 reported Media3 media button event: keyCode=86 (KEYCODE_MEDIA_STOP) in their log when pressing skip.

Changes

  • Media3SessionCallback.onMediaButtonEvent — handle KEYCODE_MEDIA_FAST_FORWARD and KEYCODE_MEDIA_REWIND as direct skip-forward / skip-back, alongside the existing KEYCODE_MEDIA_SKIP_FORWARD / KEYCODE_MEDIA_SKIP_BACKWARD cases.
  • Media3SessionCallback.onMediaButtonEvent — on Android Automotive OS, bypass the headphone multi-tap queue for KEYCODE_MEDIA_NEXT and KEYCODE_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 — extracted launchSkipForward() / 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_COMMANDS and PocketCastsForwardingPlayer.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 via ConnectionResult in onConnect so Wear OS isn't affected.

Testing Instructions

Automated tests

  1. ./gradlew :modules:services:repositories:testDebugUnitTest --tests "au.com.shiftyjelly.pocketcasts.repositories.playback.Media3SessionCallbackTest"
  2. ./gradlew :modules:services:repositories:testDebugUnitTest --tests "au.com.shiftyjelly.pocketcasts.repositories.playback.PocketCastsForwardingPlayerTest"
  3. ✅ Verify the new tests pass:
    • KEYCODE_MEDIA_FAST_FORWARD calls skipForwardSuspend directly
    • KEYCODE_MEDIA_REWIND calls skipBackwardSuspend directly
    • on automotive, KEYCODE_MEDIA_NEXT skips forward immediately
    • on automotive, KEYCODE_MEDIA_PREVIOUS skips backward immediately
  4. ✅ Verify the existing command-exposure tests still pass — SEEK_TO_NEXT / SEEK_TO_PREVIOUS are intentionally not advertised.

Manual — AAOS emulator

  1. Build the automotive variant: ./gradlew :automotive:assembleDebug.
  2. Install and launch Pocket Casts on the AAOS emulator (or a physical Android 14 AAOS vehicle if available).
  3. Start playback of any episode.
  4. Send a KEYCODE_MEDIA_NEXT key event via adb shell input keyevent 87 (and 88 for previous, 90 for fast-forward, 89 for rewind).
  5. ✅ Verify each invocation triggers a skip forward / skip backward in the playback position immediately (no 250 ms delay), and that no Media3: stop → pause log line is emitted.
  6. ✅ Verify on-screen playback controls within the app still work normally.

Manual — mobile regression check

  1. Run the mobile debug variant.
  2. Connect Pixel Buds (or any BT headset with multi-tap), play an episode, and use double-tap / triple-tap.
  3. ✅ Verify multi-tap still resolves to the user's configured headphoneControlsNextAction / headphoneControlsPreviousAction after the 250 ms window — the automotive bypass only applies when Util.isAutomotive(context) is true.

Manual — Wear OS regression check

  1. Run the Wear OS variant and connect to a paired watch.
  2. ✅ Verify the transport controls remain the original skip-forward / skip-back pair only — no duplicate "previous track / next track" buttons (see #discussion_r3286574602).

Checklist

  • Ensure the linter passes (./gradlew spotlessApply to automatically apply formatting/linting)
  • I have considered whether it makes sense to add tests for my changes
  • If this is a user-facing change, I have added an entry in CHANGELOG.md
  • All strings that need to be localized are in modules/services/localization/src/main/res/values/strings.xml (no string changes)
  • Any jetpack compose components I added or changed are covered by compose previews (no Compose changes)
  • I have updated (or requested that someone edit) the spreadsheet to reflect any new or changed analytics. (no analytics changes)

Note

Tests were not executed locally — the current main branch pulls in a crashlogging:6.0.8 dependency compiled to Java 21 class files, but libs.versions.toml still pins java = "17". Without a local JDK 21 install, :modules:services:crashlogging:compileDebugJavaWithJavac fails before any test code runs. Pre-existing issue, not caused by this PR — relying on CI for verification.

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.
@joashrajin joashrajin requested a review from a team as a code owner May 22, 2026 05:26
@joashrajin joashrajin requested review from Copilot and geekygecko and removed request for a team May 22, 2026 05:26
@dangermattic
Copy link
Copy Markdown
Collaborator

dangermattic commented May 22, 2026

1 Warning
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_PREVIOUS from 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_REWIND as direct skip forward/back in onMediaButtonEvent.
  • On AAOS, bypass the multi-tap queue for KEYCODE_MEDIA_NEXT / KEYCODE_MEDIA_PREVIOUS to 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.

@joashrajin joashrajin added [Type] Bug Not functioning as intended. [Area] Automotive Android Automotive labels May 22, 2026
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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling these actions on add devices can have issues like now the Wear OS has previous track and skip backwards.

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this better @geekygecko or do you have any other suggestions?

return true
}
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great idea.

@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.
Copilot AI review requested due to automatic review settings May 22, 2026 12:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_PREVIOUS to be absent. Either update the assertions (and production PocketCastsForwardingPlayer.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))

Comment on lines +214 to +219
// 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,
-> {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Area] Automotive Android Automotive [Type] Bug Not functioning as intended.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants