315 lines
12 KiB
Kotlin
315 lines
12 KiB
Kotlin
package org.moire.ultrasonic.util
|
|
|
|
import android.app.PendingIntent
|
|
import android.content.ComponentName
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.os.Bundle
|
|
import android.support.v4.media.MediaDescriptionCompat
|
|
import android.support.v4.media.MediaMetadataCompat
|
|
import android.support.v4.media.session.MediaSessionCompat
|
|
import android.support.v4.media.session.PlaybackStateCompat
|
|
import android.support.v4.media.session.PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN
|
|
import android.view.KeyEvent
|
|
import org.koin.core.component.KoinComponent
|
|
import org.koin.core.component.inject
|
|
import org.moire.ultrasonic.R
|
|
import org.moire.ultrasonic.domain.MusicDirectory
|
|
import org.moire.ultrasonic.domain.PlayerState
|
|
import org.moire.ultrasonic.imageloader.BitmapUtils
|
|
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver
|
|
import org.moire.ultrasonic.service.DownloadFile
|
|
import timber.log.Timber
|
|
|
|
private const val INTENT_CODE_MEDIA_BUTTON = 161
|
|
|
|
class MediaSessionHandler : KoinComponent {
|
|
|
|
private var mediaSession: MediaSessionCompat? = null
|
|
private var playbackState: Int? = null
|
|
private var playbackActions: Long? = null
|
|
private var cachedPlayingIndex: Long? = null
|
|
|
|
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
|
|
private val applicationContext by inject<Context>()
|
|
|
|
private var referenceCount: Int = 0
|
|
private var cachedPlaylist: Iterable<MusicDirectory.Entry>? = null
|
|
private var playbackPositionDelayCount: Int = 0
|
|
|
|
fun release() {
|
|
|
|
if (referenceCount > 0) referenceCount--
|
|
if (referenceCount > 0) return
|
|
|
|
mediaSession?.isActive = false
|
|
mediaSessionEventDistributor.releaseCachedMediaSessionToken()
|
|
mediaSession?.release()
|
|
mediaSession = null
|
|
|
|
Timber.i("MediaSessionHandler.initialize Media Session released")
|
|
}
|
|
|
|
fun initialize() {
|
|
|
|
referenceCount++
|
|
if (referenceCount > 1) return
|
|
|
|
@Suppress("MagicNumber")
|
|
val keycode = 110
|
|
|
|
Timber.d("MediaSessionHandler.initialize Creating Media Session")
|
|
|
|
mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService")
|
|
val mediaSessionToken = mediaSession!!.sessionToken
|
|
mediaSessionEventDistributor.raiseMediaSessionTokenCreatedEvent(mediaSessionToken!!)
|
|
|
|
updateMediaButtonReceiver()
|
|
|
|
mediaSession!!.setCallback(object : MediaSessionCompat.Callback() {
|
|
override fun onPlay() {
|
|
super.onPlay()
|
|
|
|
getPendingIntentForMediaAction(
|
|
applicationContext,
|
|
KeyEvent.KEYCODE_MEDIA_PLAY,
|
|
keycode
|
|
).send()
|
|
|
|
Timber.v("Media Session Callback: onPlay")
|
|
}
|
|
|
|
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
|
super.onPlayFromMediaId(mediaId, extras)
|
|
|
|
Timber.d("Media Session Callback: onPlayFromMediaId")
|
|
mediaSessionEventDistributor.raisePlayFromMediaIdRequestedEvent(mediaId, extras)
|
|
}
|
|
|
|
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
|
super.onPlayFromSearch(query, extras)
|
|
|
|
Timber.d("Media Session Callback: onPlayFromSearch")
|
|
mediaSessionEventDistributor.raisePlayFromSearchRequestedEvent(query, extras)
|
|
}
|
|
|
|
override fun onPause() {
|
|
super.onPause()
|
|
getPendingIntentForMediaAction(
|
|
applicationContext,
|
|
KeyEvent.KEYCODE_MEDIA_PAUSE,
|
|
keycode
|
|
).send()
|
|
Timber.v("Media Session Callback: onPause")
|
|
}
|
|
|
|
override fun onStop() {
|
|
super.onStop()
|
|
getPendingIntentForMediaAction(
|
|
applicationContext,
|
|
KeyEvent.KEYCODE_MEDIA_STOP,
|
|
keycode
|
|
).send()
|
|
Timber.v("Media Session Callback: onStop")
|
|
}
|
|
|
|
override fun onSkipToNext() {
|
|
super.onSkipToNext()
|
|
getPendingIntentForMediaAction(
|
|
applicationContext,
|
|
KeyEvent.KEYCODE_MEDIA_NEXT,
|
|
keycode
|
|
).send()
|
|
Timber.v("Media Session Callback: onSkipToNext")
|
|
}
|
|
|
|
override fun onSkipToPrevious() {
|
|
super.onSkipToPrevious()
|
|
getPendingIntentForMediaAction(
|
|
applicationContext,
|
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS,
|
|
keycode
|
|
).send()
|
|
Timber.v("Media Session Callback: onSkipToPrevious")
|
|
}
|
|
|
|
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
|
|
// This probably won't be necessary once we implement more
|
|
// of the modern media APIs, like the MediaController etc.
|
|
val event = mediaButtonEvent.extras!!["android.intent.extra.KEY_EVENT"] as KeyEvent?
|
|
mediaSessionEventDistributor.raiseMediaButtonEvent(event)
|
|
return true
|
|
}
|
|
|
|
override fun onSkipToQueueItem(id: Long) {
|
|
super.onSkipToQueueItem(id)
|
|
mediaSessionEventDistributor.raiseSkipToQueueItemRequestedEvent(id)
|
|
}
|
|
}
|
|
)
|
|
|
|
// It seems to be the best practice to set this to true for the lifetime of the session
|
|
mediaSession!!.isActive = true
|
|
if (cachedPlaylist != null) updateMediaSessionQueue(cachedPlaylist!!)
|
|
Timber.i("MediaSessionHandler.initialize Media Session created")
|
|
}
|
|
|
|
fun updateMediaSession(currentPlaying: DownloadFile?, currentPlayingIndex: Long?, playerState: PlayerState) {
|
|
Timber.d("Updating the MediaSession")
|
|
|
|
// Set Metadata
|
|
val metadata = MediaMetadataCompat.Builder()
|
|
if (currentPlaying != null) {
|
|
try {
|
|
val song = currentPlaying.song
|
|
val cover = BitmapUtils.getAlbumArtBitmapFromDisk(
|
|
song, Util.getMinDisplayMetric()
|
|
)
|
|
val duration = song.duration?.times(1000) ?: -1
|
|
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration.toLong())
|
|
metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.artist)
|
|
metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.artist)
|
|
metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album)
|
|
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title)
|
|
metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, cover)
|
|
} catch (e: Exception) {
|
|
Timber.e(e, "Error setting the metadata")
|
|
}
|
|
}
|
|
|
|
// Save the metadata
|
|
mediaSession!!.setMetadata(metadata.build())
|
|
|
|
playbackActions = PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
|
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
|
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
|
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
|
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH or
|
|
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
|
|
|
// Map our playerState to native PlaybackState
|
|
// TODO: Synchronize these APIs
|
|
when (playerState) {
|
|
PlayerState.STARTED -> {
|
|
playbackState = PlaybackStateCompat.STATE_PLAYING
|
|
playbackActions = playbackActions!! or
|
|
PlaybackStateCompat.ACTION_PAUSE or
|
|
PlaybackStateCompat.ACTION_STOP
|
|
}
|
|
PlayerState.COMPLETED,
|
|
PlayerState.STOPPED -> {
|
|
playbackState = PlaybackStateCompat.STATE_STOPPED
|
|
}
|
|
PlayerState.IDLE -> {
|
|
// IDLE state usually just means the playback is stopped
|
|
// STATE_NONE means that there is no track to play (playlist is empty)
|
|
playbackState = if (currentPlaying == null)
|
|
PlaybackStateCompat.STATE_NONE
|
|
else
|
|
PlaybackStateCompat.STATE_STOPPED
|
|
playbackActions = 0L
|
|
}
|
|
PlayerState.PAUSED -> {
|
|
playbackState = PlaybackStateCompat.STATE_PAUSED
|
|
playbackActions = playbackActions!! or
|
|
PlaybackStateCompat.ACTION_PLAY or
|
|
PlaybackStateCompat.ACTION_STOP
|
|
}
|
|
else -> {
|
|
// These are the states PREPARING, PREPARED & DOWNLOADING
|
|
playbackState = PlaybackStateCompat.STATE_PAUSED
|
|
}
|
|
}
|
|
|
|
val playbackStateBuilder = PlaybackStateCompat.Builder()
|
|
playbackStateBuilder.setState(playbackState!!, PLAYBACK_POSITION_UNKNOWN, 1.0f)
|
|
|
|
// Set actions
|
|
playbackStateBuilder.setActions(playbackActions!!)
|
|
|
|
cachedPlayingIndex = currentPlayingIndex
|
|
if (currentPlayingIndex != null)
|
|
playbackStateBuilder.setActiveQueueItemId(currentPlayingIndex)
|
|
|
|
// Save the playback state
|
|
mediaSession!!.setPlaybackState(playbackStateBuilder.build())
|
|
}
|
|
|
|
fun updateMediaSessionQueue(playlist: Iterable<MusicDirectory.Entry>)
|
|
{
|
|
// This call is cached because Downloader may initialize earlier than the MediaSession
|
|
cachedPlaylist = playlist
|
|
if (mediaSession == null) return
|
|
|
|
// TODO Implement Now Playing queue handling properly
|
|
mediaSession!!.setQueueTitle(applicationContext.getString(R.string.button_bar_now_playing))
|
|
mediaSession!!.setQueue(playlist.mapIndexed { id, song ->
|
|
MediaSessionCompat.QueueItem(
|
|
MediaDescriptionCompat.Builder()
|
|
.setTitle(song.title)
|
|
.build(), id.toLong())
|
|
})
|
|
}
|
|
|
|
fun updateMediaSessionPlaybackPosition(playbackPosition: Long) {
|
|
|
|
if (mediaSession == null) return
|
|
|
|
if (playbackState == null || playbackActions == null) return
|
|
|
|
// Playback position is updated too frequently in the player.
|
|
// This counter makes sure that the MediaSession is updated ~ at every second
|
|
playbackPositionDelayCount++
|
|
if (playbackPositionDelayCount < 10) return
|
|
|
|
playbackPositionDelayCount = 0
|
|
val playbackStateBuilder = PlaybackStateCompat.Builder()
|
|
playbackStateBuilder.setState(playbackState!!, playbackPosition, 1.0f)
|
|
playbackStateBuilder.setActions(playbackActions!!)
|
|
|
|
if (cachedPlayingIndex != null)
|
|
playbackStateBuilder.setActiveQueueItemId(cachedPlayingIndex!!)
|
|
|
|
mediaSession!!.setPlaybackState(playbackStateBuilder.build())
|
|
}
|
|
|
|
fun updateMediaButtonReceiver() {
|
|
if (Util.getMediaButtonsEnabled()) {
|
|
registerMediaButtonEventReceiver()
|
|
} else {
|
|
unregisterMediaButtonEventReceiver()
|
|
}
|
|
}
|
|
|
|
private fun registerMediaButtonEventReceiver() {
|
|
val component = ComponentName(applicationContext.packageName, MediaButtonIntentReceiver::class.java.name)
|
|
val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
|
|
mediaButtonIntent.component = component
|
|
|
|
val pendingIntent = PendingIntent.getBroadcast(
|
|
applicationContext,
|
|
INTENT_CODE_MEDIA_BUTTON,
|
|
mediaButtonIntent,
|
|
PendingIntent.FLAG_CANCEL_CURRENT
|
|
)
|
|
|
|
mediaSession?.setMediaButtonReceiver(pendingIntent)
|
|
}
|
|
|
|
private fun unregisterMediaButtonEventReceiver() {
|
|
mediaSession?.setMediaButtonReceiver(null)
|
|
}
|
|
|
|
// TODO Copied from MediaPlayerService. Move to Utils
|
|
private fun getPendingIntentForMediaAction(
|
|
context: Context,
|
|
keycode: Int,
|
|
requestCode: Int
|
|
): PendingIntent {
|
|
val intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
|
val flags = PendingIntent.FLAG_UPDATE_CURRENT
|
|
intent.setPackage(context.packageName)
|
|
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode))
|
|
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
|
}
|
|
} |