Add external media controls
Add the ability to listen to external media controls, such as through a bluetooth headset.
This commit is contained in:
parent
3251b84e23
commit
2ded706445
4 changed files with 57 additions and 12 deletions
|
@ -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'
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue