From cad4580045f83cec88ac990c3266f1de0ca368a7 Mon Sep 17 00:00:00 2001 From: Robert Stone Date: Tue, 24 Nov 2020 21:34:12 -0800 Subject: [PATCH] Enable seeking via Android Auto steering wheel controls --- .../org/videolan/vlc/MediaSessionCallback.kt | 59 ++++++++++++++++++- .../src/org/videolan/vlc/PlaybackService.kt | 2 + 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt b/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt index 4be9605bd..b959a26d1 100644 --- a/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt +++ b/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt @@ -1,10 +1,12 @@ package org.videolan.vlc +import android.annotation.SuppressLint import android.content.Intent import android.net.Uri import android.os.Bundle import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat +import android.util.Log import android.view.KeyEvent import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope @@ -23,10 +25,12 @@ import kotlin.math.min @Suppress("unused") private const val TAG = "VLC/MediaSessionCallback" +private const val TEN_SECONDS = 10000L @ObsoleteCoroutinesApi @ExperimentalCoroutinesApi internal class MediaSessionCallback(private val playbackService: PlaybackService) : MediaSessionCompat.Callback() { + private var prevActionSeek = false override fun onPlay() { if (playbackService.hasMedia()) playbackService.play() @@ -42,9 +46,60 @@ internal class MediaSessionCallback(private val playbackService: PlaybackService true } else false } + /** + * Implement fast forward and rewind behavior by directly handling the previous and next button events. + * Normally the buttons are triggered on ACTION_DOWN; however, we ignore the ACTION_DOWN event when + * isAndroidAutoHardKey returns true, and perform the operation on the ACTION_UP event instead. If the previous or + * next button is held down, a callback occurs with the long press flag set. When a long press is received, + * invoke the onFastForward() or onRewind() methods, and set the prevActionSeek flag. The ACTION_UP event + * action is bypassed if the flag is set. The prevActionSeek flag is reset to false for the next invocation. + */ + if (isAndroidAutoHardKey(keyEvent) && (keyEvent.keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || keyEvent.keyCode == KeyEvent.KEYCODE_MEDIA_NEXT)) { + when (keyEvent.action) { + KeyEvent.ACTION_DOWN -> { + if (playbackService.isSeekable && keyEvent.isLongPress) { + when (keyEvent.keyCode) { + KeyEvent.KEYCODE_MEDIA_NEXT -> onFastForward() + KeyEvent.KEYCODE_MEDIA_PREVIOUS -> onRewind() + } + prevActionSeek = true + } + } + KeyEvent.ACTION_UP -> { + if (!prevActionSeek) { + val enabledActions = playbackService.enabledActions + when (keyEvent.keyCode) { + KeyEvent.KEYCODE_MEDIA_NEXT -> if ((enabledActions and PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0L) onSkipToNext() + KeyEvent.KEYCODE_MEDIA_PREVIOUS -> if ((enabledActions and PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0L) onSkipToPrevious() + } + } + prevActionSeek = false + } + } + return true + } return super.onMediaButtonEvent(mediaButtonEvent) } + /** + * This function is based on the following KeyEvent captures. This may need to be updated if the behavior changes in the future. + * + * KeyEvent from Media Control UI: + * {action=ACTION_DOWN, keyCode=KEYCODE_MEDIA_NEXT, scanCode=0, metaState=0, flags=0x0, repeatCount=0, eventTime=0, downTime=0, deviceId=-1, source=0x0, displayId=0} + * + * KeyEvent from Android Auto Steering Wheel Control: + * {action=ACTION_DOWN, keyCode=KEYCODE_MEDIA_NEXT, scanCode=0, metaState=0, flags=0x4, repeatCount=0, eventTime=0, downTime=0, deviceId=0, source=0x0, displayId=0} + * + * KeyEvent from Android Auto Steering Wheel Control, Holding Switch (Long Press): + * {action=ACTION_DOWN, keyCode=KEYCODE_MEDIA_NEXT, scanCode=0, metaState=0, flags=0x84, repeatCount=1, eventTime=0, downTime=0, deviceId=0, source=0x0, displayId=0} + */ + @SuppressLint("LongLogTag") + private fun isAndroidAutoHardKey(keyEvent: KeyEvent): Boolean { + val carMode = AndroidDevices.isCarMode(playbackService.applicationContext) + if (carMode) Log.i(TAG, "Android Auto Key Press: $keyEvent") + return carMode && keyEvent.deviceId == 0 && (keyEvent.flags and KeyEvent.FLAG_KEEP_TOUCH_MODE != 0) + } + override fun onCustomAction(action: String?, extras: Bundle?) { when (action) { "shuffle" -> playbackService.shuffle() @@ -162,9 +217,9 @@ internal class MediaSessionCallback(private val playbackService: PlaybackService override fun onSeekTo(pos: Long) = playbackService.seek(if (pos < 0) playbackService.time + pos else pos, fromUser = true) - override fun onFastForward() = playbackService.seek(Math.min(playbackService.length, playbackService.time + 5000)) + override fun onFastForward() = playbackService.seek((playbackService.time + TEN_SECONDS).coerceAtMost(playbackService.length), fromUser = true) - override fun onRewind() = playbackService.seek(Math.max(0, playbackService.time - 5000)) + override fun onRewind() = playbackService.seek((playbackService.time - TEN_SECONDS).coerceAtLeast(0), fromUser = true) override fun onSkipToQueueItem(id: Long) { playbackService.playIndex(id.toInt()) diff --git a/application/vlc-android/src/org/videolan/vlc/PlaybackService.kt b/application/vlc-android/src/org/videolan/vlc/PlaybackService.kt index acd74010f..fc9383f4f 100644 --- a/application/vlc-android/src/org/videolan/vlc/PlaybackService.kt +++ b/application/vlc-android/src/org/videolan/vlc/PlaybackService.kt @@ -92,6 +92,7 @@ private const val TAG = "VLC/PlaybackService" class PlaybackService : MediaBrowserServiceCompat(), LifecycleOwner { private val dispatcher = ServiceLifecycleDispatcher(this) + internal var enabledActions = PLAYBACK_BASE_ACTIONS lateinit var playlistManager: PlaylistManager private set val mediaplayer: MediaPlayer @@ -927,6 +928,7 @@ class PlaybackService : MediaBrowserServiceCompat(), LifecycleOwner { val update = mediaSession.isActive != mediaIsActive updateMediaQueueSlidingWindow() mediaSession.setPlaybackState(pscb.build()) + enabledActions = actions mediaSession.isActive = mediaIsActive mediaSession.setQueueTitle(getString(R.string.music_now_playing)) if (update) {