[Android] Enable seeking via Android Auto steering wheel controls

Robert Stone git at videolan.org
Wed Nov 25 09:48:37 CET 2020


vlc-android | branch: master | Robert Stone <rhstone at gmail.com> | Tue Nov 24 21:34:12 2020 -0800| [cad4580045f83cec88ac990c3266f1de0ca368a7] | committer: Robert Stone

Enable seeking via Android Auto steering wheel controls

> https://code.videolan.org/videolan/vlc-android/commit/cad4580045f83cec88ac990c3266f1de0ca368a7
---

 .../src/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) {



More information about the Android mailing list