Updated Android Auto to use MediaPlayerService separately
Added some missing features found in the docs
This commit is contained in:
parent
db0669098c
commit
83c6b76d0a
|
@ -60,7 +60,7 @@
|
|||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.AutoMediaPlayerService"
|
||||
android:name=".service.AutoMediaBrowserService"
|
||||
android:label="Ultrasonic Auto Media Player Service"
|
||||
android:exported="true">
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import org.koin.android.ext.koin.androidContext
|
|||
import org.koin.dsl.module
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.MediaSessionEventDistributor
|
||||
import org.moire.ultrasonic.util.NowPlayingEventDistributor
|
||||
import org.moire.ultrasonic.util.PermissionUtil
|
||||
import org.moire.ultrasonic.util.ThemeChangedEventDistributor
|
||||
|
@ -17,4 +18,5 @@ val applicationModule = module {
|
|||
single { PermissionUtil(androidContext()) }
|
||||
single { NowPlayingEventDistributor() }
|
||||
single { ThemeChangedEventDistributor() }
|
||||
single { MediaSessionEventDistributor() }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.support.v4.media.MediaBrowserCompat
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import androidx.media.utils.MediaConstants
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.moire.ultrasonic.util.MediaSessionEventDistributor
|
||||
import org.moire.ultrasonic.util.MediaSessionEventListener
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
const val MY_MEDIA_ROOT_ID = "MY_MEDIA_ROOT_ID"
|
||||
const val MY_MEDIA_ALBUM_ID = "MY_MEDIA_ALBUM_ID"
|
||||
const val MY_MEDIA_ARTIST_ID = "MY_MEDIA_ARTIST_ID"
|
||||
const val MY_MEDIA_ALBUM_ITEM = "MY_MEDIA_ALBUM_ITEM"
|
||||
const val MY_MEDIA_LIBRARY_ID = "MY_MEDIA_LIBRARY_ID"
|
||||
const val MY_MEDIA_PLAYLIST_ID = "MY_MEDIA_PLAYLIST_ID"
|
||||
|
||||
class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||
|
||||
private lateinit var mediaSessionEventListener: MediaSessionEventListener
|
||||
private val mediaSessionEventDistributor: MediaSessionEventDistributor by inject()
|
||||
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
mediaSessionEventListener = object : MediaSessionEventListener {
|
||||
override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {
|
||||
Timber.i("AutoMediaBrowserService onMediaSessionTokenCreated called")
|
||||
if (sessionToken == null) {
|
||||
Timber.i("AutoMediaBrowserService onMediaSessionTokenCreated session token was null, set it to %s", token.toString())
|
||||
sessionToken = token
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) {
|
||||
// TODO implement
|
||||
Timber.i("AutoMediaBrowserService onPlayFromMediaIdRequested called")
|
||||
}
|
||||
|
||||
override fun onPlayFromSearchRequested(query: String?, extras: Bundle?) {
|
||||
// TODO implement
|
||||
Timber.i("AutoMediaBrowserService onPlayFromSearchRequested called")
|
||||
}
|
||||
}
|
||||
|
||||
mediaSessionEventDistributor.subscribe(mediaSessionEventListener)
|
||||
|
||||
val handler = Handler()
|
||||
handler.postDelayed({
|
||||
Timber.i("AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService...")
|
||||
// TODO it seems Android Auto handles autostart, but we must check that
|
||||
lifecycleSupport.onCreate()
|
||||
MediaPlayerService.getInstance()
|
||||
}, 100)
|
||||
|
||||
Timber.i("AutoMediaBrowserService onCreate called")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
|
||||
Timber.i("AutoMediaBrowserService onDestroy called")
|
||||
}
|
||||
|
||||
override fun onGetRoot(
|
||||
clientPackageName: String,
|
||||
clientUid: Int,
|
||||
rootHints: Bundle?
|
||||
): BrowserRoot? {
|
||||
Timber.i("AutoMediaBrowserService onGetRoot called")
|
||||
|
||||
// TODO: The number of horizontal items available on the Andoid Auto screen. Check and handle.
|
||||
val maximumRootChildLimit = rootHints!!.getInt(
|
||||
MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
|
||||
4
|
||||
)
|
||||
|
||||
// TODO: The type of the horizontal items children on the Android Auto screen. Check and handle.
|
||||
val supportedRootChildFlags = rootHints!!.getInt(
|
||||
MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
|
||||
val extras = Bundle()
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM)
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM)
|
||||
|
||||
return BrowserRoot(MY_MEDIA_ROOT_ID, extras)
|
||||
}
|
||||
|
||||
override fun onLoadChildren(
|
||||
parentId: String,
|
||||
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
|
||||
) {
|
||||
Timber.i("AutoMediaBrowserService onLoadChildren called")
|
||||
|
||||
if (parentId == MY_MEDIA_ROOT_ID) {
|
||||
return getRootItems(result)
|
||||
} else {
|
||||
return getAlbumLists(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSearch(
|
||||
query: String,
|
||||
extras: Bundle?,
|
||||
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
|
||||
) {
|
||||
super.onSearch(query, extras, result)
|
||||
}
|
||||
|
||||
private fun getRootItems(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||
|
||||
// TODO implement this with proper texts, icons, etc
|
||||
mediaItems.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Library")
|
||||
.setMediaId(MY_MEDIA_LIBRARY_ID)
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
|
||||
mediaItems.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Artists")
|
||||
.setMediaId(MY_MEDIA_ARTIST_ID)
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
|
||||
mediaItems.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Albums")
|
||||
.setMediaId(MY_MEDIA_ALBUM_ID)
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
|
||||
mediaItems.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Playlists")
|
||||
.setMediaId(MY_MEDIA_PLAYLIST_ID)
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
|
||||
result.sendResult(mediaItems)
|
||||
}
|
||||
|
||||
private fun getAlbumLists(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||
|
||||
val description = MediaDescriptionCompat.Builder()
|
||||
.setTitle("Test")
|
||||
.setMediaId(MY_MEDIA_ALBUM_ITEM + 1)
|
||||
.build()
|
||||
|
||||
mediaItems.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
description,
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
|
||||
result.sendResult(mediaItems)
|
||||
}
|
||||
}
|
|
@ -171,7 +171,7 @@ class LocalMediaPlayer(
|
|||
val mainHandler = Handler(context.mainLooper)
|
||||
|
||||
val myRunnable = Runnable {
|
||||
onPlayerStateChanged!!(playerState, currentPlaying)
|
||||
onPlayerStateChanged?.invoke(playerState, currentPlaying)
|
||||
}
|
||||
mainHandler.post(myRunnable)
|
||||
}
|
||||
|
|
|
@ -14,14 +14,13 @@ import android.content.Intent
|
|||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.support.v4.media.MediaBrowserCompat
|
||||
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.view.KeyEvent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import kotlin.collections.ArrayList
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.moire.ultrasonic.R
|
||||
|
@ -37,7 +36,12 @@ import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3
|
|||
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4
|
||||
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.*
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.MediaSessionEventDistributor
|
||||
import org.moire.ultrasonic.util.NowPlayingEventDistributor
|
||||
import org.moire.ultrasonic.util.ShufflePlayBuffer
|
||||
import org.moire.ultrasonic.util.SimpleServiceBinder
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
|
@ -56,9 +60,10 @@ class MediaPlayerService : Service() {
|
|||
private val localMediaPlayer by inject<LocalMediaPlayer>()
|
||||
private val nowPlayingEventDistributor by inject<NowPlayingEventDistributor>()
|
||||
private val mediaPlayerLifecycleSupport by inject<MediaPlayerLifecycleSupport>()
|
||||
private val mediaSessionEventDistributor: MediaSessionEventDistributor by inject()
|
||||
|
||||
private var mediaSession: MediaSessionCompat? = null
|
||||
private var mediaSessionToken: MediaSessionCompat.Token? = null
|
||||
var mediaSessionToken: MediaSessionCompat.Token? = null
|
||||
private var isInForeground = false
|
||||
private var notificationBuilder: NotificationCompat.Builder? = null
|
||||
|
||||
|
@ -91,6 +96,12 @@ class MediaPlayerService : Service() {
|
|||
|
||||
localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() }
|
||||
|
||||
// TODO maybe MediaSession must be in an independent class after all...
|
||||
// It seems this must be initialized in the stopped state too, e.g. for Android Auto.
|
||||
// So it is best to init this early.
|
||||
initMediaSessions()
|
||||
updateMediaSession(null, PlayerState.IDLE)
|
||||
|
||||
// Create Notification Channel
|
||||
createNotificationChannel()
|
||||
|
||||
|
@ -113,6 +124,8 @@ class MediaPlayerService : Service() {
|
|||
localMediaPlayer.release()
|
||||
downloader.stop()
|
||||
shufflePlayBuffer.onDestroy()
|
||||
|
||||
mediaSessionEventDistributor.ReleaseCachedMediaSessionToken()
|
||||
mediaSession?.release()
|
||||
mediaSession = null
|
||||
} catch (ignored: Throwable) {
|
||||
|
@ -467,7 +480,7 @@ class MediaPlayerService : Service() {
|
|||
fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState) {
|
||||
Timber.d("Updating the MediaSession")
|
||||
|
||||
if (mediaSession == null) initMediaSessions()
|
||||
val playbackState = PlaybackStateCompat.Builder()
|
||||
|
||||
// Set Metadata
|
||||
val metadata = MediaMetadataCompat.Builder()
|
||||
|
@ -483,6 +496,9 @@ class MediaPlayerService : Service() {
|
|||
metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album)
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title)
|
||||
metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, cover)
|
||||
|
||||
playbackState.setActiveQueueItemId(downloader.currentPlayingIndex.toLong())
|
||||
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error setting the metadata")
|
||||
}
|
||||
|
@ -492,13 +508,15 @@ class MediaPlayerService : Service() {
|
|||
mediaSession!!.setMetadata(metadata.build())
|
||||
|
||||
// Create playback State
|
||||
val playbackState = PlaybackStateCompat.Builder()
|
||||
val state: Int
|
||||
val isActive: Boolean
|
||||
|
||||
var actions: Long = PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||
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
|
||||
|
@ -534,7 +552,8 @@ class MediaPlayerService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
playbackState.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f)
|
||||
// TODO playerPosition should be updated more frequently (currently this function is called only when the playing track changes)
|
||||
playbackState.setState(state, playerPosition.toLong(), 1.0f)
|
||||
|
||||
// Set actions
|
||||
playbackState.setActions(actions)
|
||||
|
@ -545,6 +564,14 @@ class MediaPlayerService : Service() {
|
|||
// Set Active state
|
||||
mediaSession!!.isActive = isActive
|
||||
|
||||
// TODO Implement Now Playing queue handling properly
|
||||
mediaSession!!.setQueueTitle("Now Playing")
|
||||
mediaSession!!.setQueue(downloader.downloadList.mapIndexed { id, file ->
|
||||
MediaSessionCompat.QueueItem(MediaDescriptionCompat.Builder()
|
||||
.setTitle(file.song.title)
|
||||
.build(), id.toLong())
|
||||
})
|
||||
|
||||
Timber.d("Setting the MediaSession to active = %s", isActive)
|
||||
}
|
||||
|
||||
|
@ -795,6 +822,7 @@ class MediaPlayerService : Service() {
|
|||
|
||||
mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService")
|
||||
mediaSessionToken = mediaSession!!.sessionToken
|
||||
mediaSessionEventDistributor.RaiseMediaSessionTokenCreatedEvent(mediaSessionToken!!)
|
||||
|
||||
updateMediaButtonReceiver()
|
||||
|
||||
|
@ -810,35 +838,21 @@ class MediaPlayerService : Service() {
|
|||
|
||||
Timber.v("Media Session Callback: onPlay")
|
||||
}
|
||||
/*
|
||||
|
||||
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||
super.onPlayFromMediaId(mediaId, extras)
|
||||
|
||||
val result = autoMediaBrowser!!.getBundleData(extras)
|
||||
if (result != null) {
|
||||
val mediaId = result.first
|
||||
val directoryList = result.second
|
||||
Timber.d("Media Session Callback: onPlayFromMediaId")
|
||||
mediaSessionEventDistributor.RaisePlayFromMediaIdRequestedEvent(mediaId, extras)
|
||||
}
|
||||
|
||||
resetPlayback()
|
||||
val songs: MutableList<MusicDirectory.Entry> = mutableListOf()
|
||||
var found = false
|
||||
for (item in directoryList) {
|
||||
if (found || item.id == mediaId) {
|
||||
found = true
|
||||
songs.add(item)
|
||||
}
|
||||
}
|
||||
downloader.download(songs, false, false, false, true)
|
||||
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||
super.onPlayFromSearch(query, extras)
|
||||
|
||||
getPendingIntentForMediaAction(
|
||||
applicationContext,
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY,
|
||||
keycode
|
||||
).send()
|
||||
Timber.d("Media Session Callback: onPlayFromSearch")
|
||||
mediaSessionEventDistributor.RaisePlayFromSearchRequestedEvent(query, extras)
|
||||
}
|
||||
Timber.v("Media Session Callback: onPlayFromMediaId")
|
||||
}
|
||||
*/
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
getPendingIntentForMediaAction(
|
||||
|
@ -886,6 +900,11 @@ class MediaPlayerService : Service() {
|
|||
mediaPlayerLifecycleSupport.handleKeyEvent(event)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSkipToQueueItem(id: Long) {
|
||||
super.onSkipToQueueItem(id)
|
||||
play(id.toInt())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
package org.moire.ultrasonic.util
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
|
||||
/**
|
||||
* This class distributes MediaSession related events to its subscribers.
|
||||
* It is a primitive implementation of a pub-sub event bus
|
||||
*/
|
||||
class MediaSessionEventDistributor {
|
||||
var eventListenerList: MutableList<MediaSessionEventListener> =
|
||||
listOf<MediaSessionEventListener>().toMutableList()
|
||||
|
||||
var cachedToken: MediaSessionCompat.Token? = null
|
||||
|
||||
fun subscribe(listener: MediaSessionEventListener) {
|
||||
eventListenerList.add(listener)
|
||||
|
||||
synchronized(this) {
|
||||
if (cachedToken != null)
|
||||
listener.onMediaSessionTokenCreated(cachedToken!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun unsubscribe(listener: MediaSessionEventListener) {
|
||||
eventListenerList.remove(listener)
|
||||
}
|
||||
|
||||
fun ReleaseCachedMediaSessionToken() {
|
||||
synchronized(this) {
|
||||
cachedToken = null
|
||||
}
|
||||
}
|
||||
|
||||
fun RaiseMediaSessionTokenCreatedEvent(token: MediaSessionCompat.Token) {
|
||||
synchronized(this) {
|
||||
cachedToken = token
|
||||
eventListenerList.forEach { listener -> listener.onMediaSessionTokenCreated(token) }
|
||||
}
|
||||
}
|
||||
|
||||
fun RaisePlayFromMediaIdRequestedEvent(mediaId: String?, extras: Bundle?) {
|
||||
eventListenerList.forEach { listener -> listener.onPlayFromMediaIdRequested(mediaId, extras) }
|
||||
}
|
||||
|
||||
fun RaisePlayFromSearchRequestedEvent(query: String?, extras: Bundle?) {
|
||||
eventListenerList.forEach { listener -> listener.onPlayFromSearchRequested(query, extras) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.moire.ultrasonic.util
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
|
||||
/**
|
||||
* Callback interface for MediaSession related event subscribers
|
||||
*/
|
||||
interface MediaSessionEventListener {
|
||||
fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token)
|
||||
fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?)
|
||||
fun onPlayFromSearchRequested(query: String?, extras: Bundle?)
|
||||
}
|
Loading…
Reference in New Issue