Add external media controls

Add the ability to listen to external media controls, such as through a bluetooth headset.
This commit is contained in:
OxygenCobalt 2020-10-26 20:08:22 -06:00
parent 3251b84e23
commit 2ded706445
4 changed files with 57 additions and 12 deletions

View file

@ -69,6 +69,9 @@ dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
// Media
implementation 'androidx.media:media:1.2.0'
// --- THIRD PARTY ---
// Image loading
@ -81,7 +84,9 @@ dependencies {
ktlint "com.pinterest:ktlint:0.37.2"
// ExoPlayer
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.1'
def exoplayer_version = "2.12.1"
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:extension-mediasession:$exoplayer_version"
// Memory Leak checking
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'

View file

@ -25,6 +25,8 @@ class CompactPlaybackFragment : Fragment() {
): View? {
val binding = FragmentCompactPlaybackBinding.inflate(inflater)
// FIXME: Prevent the play/pause icon from animating on startup
// [requires new callback from PlaybackStateManager]
val iconPauseToPlay = ContextCompat.getDrawable(
requireContext(), R.drawable.ic_pause_to_play
) as AnimatedVectorDrawable
@ -60,7 +62,7 @@ class CompactPlaybackFragment : Fragment() {
}
playbackModel.isPlaying.observe(viewLifecycleOwner) {
if (playbackModel.canAnimate) {
if (true) {
if (it) {
// Animate the icon transition when the playing status switches
binding.playbackControls.setImageDrawable(iconPauseToPlay)
@ -90,6 +92,6 @@ class CompactPlaybackFragment : Fragment() {
override fun onPause() {
super.onPause()
playbackModel.resetAnimStatus()
// playbackModel.resetAnimStatus()
}
}

View file

@ -8,14 +8,19 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.Parcelable
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log
import android.view.KeyEvent
import androidx.core.app.NotificationCompat
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.conflate
@ -44,6 +49,14 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
}
private val playbackManager = PlaybackStateManager.getInstance()
private lateinit var mediaSession: MediaSessionCompat
private val buttonMediaCallback = object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
Log.d(this::class.simpleName, "Hello?")
return true
}
}
private val serviceJob = Job()
private val serviceScope = CoroutineScope(
@ -67,6 +80,39 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
override fun onCreate() {
super.onCreate()
mediaSession = MediaSessionCompat(this, packageName).apply {
isActive = true
}
val connector = MediaSessionConnector(mediaSession)
connector.setPlayer(player)
connector.setMediaButtonEventHandler { _, _, mediaButtonEvent ->
val item = mediaButtonEvent
.getParcelableExtra<Parcelable>(Intent.EXTRA_KEY_EVENT) as KeyEvent
if (item.action == KeyEvent.ACTION_DOWN) {
when (item.keyCode) {
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
playbackManager.next()
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
playbackManager.prev()
}
// TODO: Implement the other callbacks for
// CLOSE/STOP & REWIND
}
}
true
}
notification = createNotification()
playbackManager.addCallback(this)
@ -76,6 +122,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
super.onDestroy()
player.release()
mediaSession.release()
serviceJob.cancel()
playbackManager.removeCallback(this)

View file

@ -56,9 +56,6 @@ class PlaybackViewModel() : ViewModel(), PlaybackStateCallback {
it.slice((mIndex.value!! + 1) until it.size)
}
private var mCanAnimate = false
val canAnimate: Boolean get() = mCanAnimate
// Service setup
private val playbackManager = PlaybackStateManager.getInstance()
@ -168,8 +165,6 @@ class PlaybackViewModel() : ViewModel(), PlaybackStateCallback {
// Flip the playing status.
fun invertPlayingStatus() {
mCanAnimate = true
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
}
@ -180,10 +175,6 @@ class PlaybackViewModel() : ViewModel(), PlaybackStateCallback {
// --- OTHER FUNCTIONS ---
fun resetAnimStatus() {
mCanAnimate = false
}
fun setSeekingStatus(value: Boolean) {
mIsSeeking.value = value
}