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-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version" implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
// Media
implementation 'androidx.media:media:1.2.0'
// --- THIRD PARTY --- // --- THIRD PARTY ---
// Image loading // Image loading
@ -81,7 +84,9 @@ dependencies {
ktlint "com.pinterest:ktlint:0.37.2" ktlint "com.pinterest:ktlint:0.37.2"
// ExoPlayer // 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 // Memory Leak checking
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'

View file

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

View file

@ -8,14 +8,19 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.Parcelable
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log import android.util.Log
import android.view.KeyEvent
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
@ -44,6 +49,14 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
} }
private val playbackManager = PlaybackStateManager.getInstance() 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 serviceJob = Job()
private val serviceScope = CoroutineScope( private val serviceScope = CoroutineScope(
@ -67,6 +80,39 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
override fun onCreate() { override fun onCreate() {
super.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() notification = createNotification()
playbackManager.addCallback(this) playbackManager.addCallback(this)
@ -76,6 +122,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
super.onDestroy() super.onDestroy()
player.release() player.release()
mediaSession.release()
serviceJob.cancel() serviceJob.cancel()
playbackManager.removeCallback(this) playbackManager.removeCallback(this)

View file

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