diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml
index 7dcd1c7c..fb24dcca 100644
--- a/ultrasonic/src/main/AndroidManifest.xml
+++ b/ultrasonic/src/main/AndroidManifest.xml
@@ -3,6 +3,8 @@
package="org.moire.ultrasonic"
android:installLocation="auto">
+
+
@@ -27,6 +29,14 @@
android:name=".app.UApp"
android:label="@string/common.appname"
android:usesCleartextTraffic="true">
+
+
+
+
+
+
+ android:exported="true">
+
+
+
+
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt
index 0e4c2503..c3e80f4f 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt
@@ -125,6 +125,10 @@ class LocalMediaPlayer(
}
fun release() {
+ // Calling reset() will result in changing this player's state. If we allow
+ // the onPlayerStateChanged callback, then the state change will cause this
+ // to resurrect the media session which has just been destroyed.
+ onPlayerStateChanged = null
reset()
try {
val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt
index fdc3b4cb..46240783 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt
@@ -11,18 +11,27 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
-import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
-import android.os.IBinder
+import android.os.Bundle
+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 java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import kotlin.collections.ArrayList
import org.koin.android.ext.android.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.activity.NavigationActivity
@@ -40,7 +49,6 @@ import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil
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
@@ -49,8 +57,7 @@ import timber.log.Timber
* while the rest of the Ultrasonic App is in the background.
*/
@Suppress("LargeClass")
-class MediaPlayerService : Service() {
- private val binder: IBinder = SimpleServiceBinder(this)
+class MediaPlayerService : MediaBrowserServiceCompat() {
private val scrobbler = Scrobbler()
private val jukeboxMediaPlayer by inject()
@@ -62,20 +69,25 @@ class MediaPlayerService : Service() {
private val mediaPlayerLifecycleSupport by inject()
private var mediaSession: MediaSessionCompat? = null
- private var mediaSessionToken: MediaSessionCompat.Token? = null
private var isInForeground = false
private var notificationBuilder: NotificationCompat.Builder? = null
+ val executorService: ExecutorService = Executors.newFixedThreadPool(4)
+
+ private val MEDIA_BROWSER_ROOT_ID = "_Ultrasonice_mb_root_"
+ private val MEDIA_BROWSER_ALBUM_LIST_ROOT = "_Ultrasonic_mb_album_list_root_"
+ private val MEDIA_BROWSER_ALBUM_PREFIX = "_Ultrasonic_mb_album_prefix_"
+ private val MEDIA_BROWSER_EXTRA_ENTRY_BYTES = "_Ultrasonic_mb_extra_entry_bytes_"
+
private val repeatMode: RepeatMode
get() = Util.getRepeatMode()
- override fun onBind(intent: Intent): IBinder {
- return binder
- }
-
override fun onCreate() {
super.onCreate()
+ updateMediaSession(null, PlayerState.IDLE)
+ mediaSession!!.isActive = true
+
downloader.onCreate()
shufflePlayBuffer.onCreate()
localMediaPlayer.init()
@@ -136,6 +148,99 @@ class MediaPlayerService : Service() {
}
}
+ override fun onGetRoot(
+ clientPackageName: String,
+ clientUid: Int,
+ rootHints: Bundle?
+ ): MediaBrowserServiceCompat.BrowserRoot {
+
+ // Returns a root ID that clients can use with onLoadChildren() to retrieve
+ // the content hierarchy. Note that this root isn't actually displayed.
+ return MediaBrowserServiceCompat.BrowserRoot(MEDIA_BROWSER_ROOT_ID, null)
+ }
+
+ override fun onLoadChildren(
+ parentMediaId: String,
+ result: MediaBrowserServiceCompat.Result>
+ ) {
+
+ val mediaItems: MutableList = mutableListOf()
+
+ if (MEDIA_BROWSER_ROOT_ID == parentMediaId) {
+ // Build the MediaItem objects for the top level,
+ // and put them in the mediaItems list...
+
+ var albumList: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder()
+ albumList.setTitle("Browse Albums").setMediaId(MEDIA_BROWSER_ALBUM_LIST_ROOT)
+ mediaItems.add(
+ MediaBrowserCompat.MediaItem(
+ albumList.build(),
+ MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
+ )
+ )
+ } else if (MEDIA_BROWSER_ALBUM_LIST_ROOT == parentMediaId) {
+ executorService.execute {
+ val musicService = getMusicService()
+
+ val musicDirectory: MusicDirectory = musicService.getAlbumList2(
+ "alphabeticalByName", 10, 0, null
+ )
+
+ for (item in musicDirectory.getAllChild()) {
+ var entryBuilder: MediaDescriptionCompat.Builder =
+ MediaDescriptionCompat.Builder()
+ entryBuilder
+ .setTitle(item.title)
+ .setMediaId(MEDIA_BROWSER_ALBUM_PREFIX + item.id)
+ mediaItems.add(
+ MediaBrowserCompat.MediaItem(
+ entryBuilder.build(),
+ MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
+ )
+ )
+ }
+ result.sendResult(mediaItems)
+ }
+ result.detach()
+ return
+ } else if (parentMediaId.startsWith(MEDIA_BROWSER_ALBUM_PREFIX)) {
+ executorService.execute {
+ val musicService = getMusicService()
+ val id = parentMediaId.substring(MEDIA_BROWSER_ALBUM_PREFIX.length)
+
+ val albumDirectory = musicService.getAlbum(
+ id, "", false
+ )
+ for (item in albumDirectory.getAllChild()) {
+ var extras = Bundle()
+
+ var baos = ByteArrayOutputStream()
+ var oos = ObjectOutputStream(baos)
+ oos.writeObject(item)
+ oos.close()
+ extras.putByteArray(MEDIA_BROWSER_EXTRA_ENTRY_BYTES, baos.toByteArray())
+
+ var entryBuilder: MediaDescriptionCompat.Builder =
+ MediaDescriptionCompat.Builder()
+ entryBuilder.setTitle(item.title).setMediaId(item.id).setExtras(extras)
+ mediaItems.add(
+ MediaBrowserCompat.MediaItem(
+ entryBuilder.build(),
+ MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
+ )
+ )
+ }
+ result.sendResult(mediaItems)
+ }
+ result.detach()
+ return
+ } else {
+ // Examine the passed parentMediaId to see which submenu we're at,
+ // and put the children of that menu in the mediaItems list...
+ }
+ result.sendResult(mediaItems)
+ }
+
@Synchronized
fun seekTo(position: Int) {
if (jukeboxMediaPlayer.isEnabled) {
@@ -631,8 +736,8 @@ class MediaPlayerService : Service() {
// Use the Media Style, to enable native Android support for playback notification
val style = androidx.media.app.NotificationCompat.MediaStyle()
- if (mediaSessionToken != null) {
- style.setMediaSession(mediaSessionToken)
+ if (getSessionToken() != null) {
+ style.setMediaSession(getSessionToken())
}
// Clear old actions
@@ -799,7 +904,7 @@ class MediaPlayerService : Service() {
Timber.w("Creating media session")
mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService")
- mediaSessionToken = mediaSession!!.sessionToken
+ setSessionToken(mediaSession!!.sessionToken)
updateMediaButtonReceiver()
@@ -816,6 +921,32 @@ class MediaPlayerService : Service() {
Timber.v("Media Session Callback: onPlay")
}
+ override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
+ super.onPlayFromMediaId(mediaId, extras)
+
+ if (extras!!.containsKey(MEDIA_BROWSER_EXTRA_ENTRY_BYTES)) {
+
+ resetPlayback()
+
+ var bytes = extras.getByteArray(MEDIA_BROWSER_EXTRA_ENTRY_BYTES)
+ var bais = ByteArrayInputStream(bytes)
+ var ois = ObjectInputStream(bais)
+ var item: MusicDirectory.Entry = ois.readObject() as MusicDirectory.Entry
+
+ val songs: MutableList = mutableListOf()
+ songs.add(item)
+
+ downloader.download(songs, false, false, false, true)
+
+ getPendingIntentForMediaAction(
+ applicationContext,
+ KeyEvent.KEYCODE_MEDIA_PLAY,
+ keycode
+ ).send()
+ }
+ Timber.v("Media Session Callback: onPlayFromMediaId")
+ }
+
override fun onPause() {
super.onPause()
getPendingIntentForMediaAction(
diff --git a/ultrasonic/src/main/res/xml/automotive_app_desc.xml b/ultrasonic/src/main/res/xml/automotive_app_desc.xml
new file mode 100644
index 00000000..0f485746
--- /dev/null
+++ b/ultrasonic/src/main/res/xml/automotive_app_desc.xml
@@ -0,0 +1,3 @@
+
+
+