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 46240783..9893d3ae 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -17,7 +17,6 @@ import android.content.Intent import android.os.Build 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 @@ -25,12 +24,6 @@ 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 @@ -45,6 +38,7 @@ 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.AndroidAutoMediaBrowser import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.NowPlayingEventDistributor @@ -67,18 +61,12 @@ class MediaPlayerService : MediaBrowserServiceCompat() { private val localMediaPlayer by inject() private val nowPlayingEventDistributor by inject() private val mediaPlayerLifecycleSupport by inject() + private val autoMediaBrowser: AndroidAutoMediaBrowser = AndroidAutoMediaBrowser() private var mediaSession: MediaSessionCompat? = 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() @@ -86,7 +74,6 @@ class MediaPlayerService : MediaBrowserServiceCompat() { super.onCreate() updateMediaSession(null, PlayerState.IDLE) - mediaSession!!.isActive = true downloader.onCreate() shufflePlayBuffer.onCreate() @@ -153,92 +140,14 @@ class MediaPlayerService : MediaBrowserServiceCompat() { 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) + return autoMediaBrowser.getRoot(clientPackageName, clientUid, rootHints) } 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) + autoMediaBrowser.loadChildren(parentMediaId, result) } @Synchronized @@ -924,15 +833,9 @@ class MediaPlayerService : MediaBrowserServiceCompat() { override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) { super.onPlayFromMediaId(mediaId, extras) - if (extras!!.containsKey(MEDIA_BROWSER_EXTRA_ENTRY_BYTES)) { - + val item: MusicDirectory.Entry? = autoMediaBrowser.getMusicDirectoryEntry(extras) + if (item != null) { 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) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/AndroidAutoMediaBrowser.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/AndroidAutoMediaBrowser.kt new file mode 100644 index 00000000..c76bcf35 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/AndroidAutoMediaBrowser.kt @@ -0,0 +1,225 @@ +package org.moire.ultrasonic.util + +import android.os.Bundle +import android.support.v4.media.MediaBrowserCompat +import android.support.v4.media.MediaDescriptionCompat +import androidx.media.MediaBrowserServiceCompat +import androidx.media.utils.MediaConstants +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 org.moire.ultrasonic.api.subsonic.models.AlbumListType +import org.moire.ultrasonic.domain.Genre +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.service.MusicServiceFactory + +class AndroidAutoMediaBrowser() { + + val executorService: ExecutorService = Executors.newFixedThreadPool(4) + var maximumRootChildLimit: Int = 4 + + private val MEDIA_BROWSER_ROOT_ID = "_Ultrasonice_mb_root_" + + private val MEDIA_BROWSER_GENRE_LIST_ROOT = "_Ultrasonic_mb_genre_list_root_" + private val MEDIA_BROWSER_RECENT_LIST_ROOT = "_Ultrasonic_mb_recent_list_root_" + private val MEDIA_BROWSER_ALBUM_LIST_ROOT = "_Ultrasonic_mb_album_list_root_" + private val MEDIA_BROWSER_ARTIST_LIST_ROOT = "_Ultrasonic_mb_rtist_list_root_" + + private val MEDIA_BROWSER_GENRE_PREFIX = "_Ultrasonic_mb_genre_prefix_" + private val MEDIA_BROWSER_RECENT_PREFIX = "_Ultrasonic_mb_recent_prefix_" + private val MEDIA_BROWSER_ALBUM_PREFIX = "_Ultrasonic_mb_album_prefix_" + private val MEDIA_BROWSER_ARTIST_PREFIX = "_Ultrasonic_mb_artist_prefix_" + + private val MEDIA_BROWSER_EXTRA_ENTRY_BYTES = "_Ultrasonic_mb_extra_entry_bytes_" + + fun getRoot( + clientPackageName: String, + clientUid: Int, + rootHints: Bundle? + ): MediaBrowserServiceCompat.BrowserRoot { + if (rootHints != null) { + maximumRootChildLimit = rootHints.getInt( + MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, + 4 + ) + } + // opt into the root tabs (because it's gonna be non-optional + // real soon anyway) + val extras = Bundle() + val TABS_OPT_IN_HINT = "android.media.browse.AUTO_TABS_OPT_IN_HINT" + extras.putBoolean(TABS_OPT_IN_HINT, true) + return MediaBrowserServiceCompat.BrowserRoot(MEDIA_BROWSER_ROOT_ID, extras) + } + + fun loadChildren( + 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 genreList: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder() + genreList.setTitle("Genre").setMediaId(MEDIA_BROWSER_GENRE_LIST_ROOT) + mediaItems.add( + MediaBrowserCompat.MediaItem( + genreList.build(), + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + ) + var recentList: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder() + recentList.setTitle("Recent").setMediaId(MEDIA_BROWSER_RECENT_LIST_ROOT) + mediaItems.add( + MediaBrowserCompat.MediaItem( + recentList.build(), + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + ) + var albumList: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder() + albumList.setTitle("Albums").setMediaId(MEDIA_BROWSER_ALBUM_LIST_ROOT) + mediaItems.add( + MediaBrowserCompat.MediaItem( + albumList.build(), + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + ) + var artistList: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder() + artistList.setTitle("Artists").setMediaId(MEDIA_BROWSER_ARTIST_LIST_ROOT) + mediaItems.add( + MediaBrowserCompat.MediaItem( + artistList.build(), + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + ) + } else if (MEDIA_BROWSER_GENRE_LIST_ROOT == parentMediaId) { + fetchGenres(result) + return + } else if (MEDIA_BROWSER_RECENT_LIST_ROOT == parentMediaId) { + fetchAlbumList(AlbumListType.RECENT, MEDIA_BROWSER_RECENT_PREFIX, result) + return + } else if (MEDIA_BROWSER_ALBUM_LIST_ROOT == parentMediaId) { + fetchAlbumList(AlbumListType.SORTED_BY_NAME, MEDIA_BROWSER_ALBUM_PREFIX, result) + return + } else if (MEDIA_BROWSER_ARTIST_LIST_ROOT == parentMediaId) { + fetchAlbumList(AlbumListType.SORTED_BY_ARTIST, MEDIA_BROWSER_ARTIST_PREFIX, result) + return + } else if (parentMediaId.startsWith(MEDIA_BROWSER_ALBUM_PREFIX)) { + executorService.execute { + val musicService = MusicServiceFactory.getMusicService() + val id = parentMediaId.substring(MEDIA_BROWSER_ALBUM_PREFIX.length) + + val albumDirectory = musicService.getAlbum( + id, "", false + ) + for (item in albumDirectory.getAllChild()) { + val extras = Bundle() + + // Note that Bundle supports putSerializable and MusicDirectory.Entry + // implements Serializable, but when I try to use it the app crashes + val byteArrayOutputStream = ByteArrayOutputStream() + val objectOutputStream = ObjectOutputStream(byteArrayOutputStream) + objectOutputStream.writeObject(item) + objectOutputStream.close() + extras.putByteArray( + MEDIA_BROWSER_EXTRA_ENTRY_BYTES, + byteArrayOutputStream.toByteArray() + ) + + val 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) + } + + fun getMusicDirectoryEntry(bundle: Bundle?): MusicDirectory.Entry? { + if (bundle == null) { + return null + } + + if (!bundle.containsKey(MEDIA_BROWSER_EXTRA_ENTRY_BYTES)) { + return null + } + val bytes = bundle.getByteArray(MEDIA_BROWSER_EXTRA_ENTRY_BYTES) + val byteArrayInputStream = ByteArrayInputStream(bytes) + val objectInputStream = ObjectInputStream(byteArrayInputStream) + return objectInputStream.readObject() as MusicDirectory.Entry + } + + fun fetchAlbumList( + type: AlbumListType, + idPrefix: String, + result: MediaBrowserServiceCompat.Result> + ) { + executorService.execute { + val mediaItems: MutableList = mutableListOf() + val musicService = MusicServiceFactory.getMusicService() + + val musicDirectory: MusicDirectory = musicService.getAlbumList2( + type.toString(), 500, 0, null + ) + + for (item in musicDirectory.getAllChild()) { + var entryBuilder: MediaDescriptionCompat.Builder = + MediaDescriptionCompat.Builder() + entryBuilder + .setTitle(item.title) + .setMediaId(idPrefix + item.id) + mediaItems.add( + MediaBrowserCompat.MediaItem( + entryBuilder.build(), + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + ) + } + result.sendResult(mediaItems) + } + result.detach() + } + + fun fetchGenres(result: MediaBrowserServiceCompat.Result>) { + executorService.execute { + val mediaItems: MutableList = mutableListOf() + val musicService = MusicServiceFactory.getMusicService() + + val genreList: List? = musicService.getGenres(false) + if (genreList != null) { + for (genre in genreList) { + var entryBuilder: MediaDescriptionCompat.Builder = + MediaDescriptionCompat.Builder() + entryBuilder + .setTitle(genre.name) + .setMediaId(MEDIA_BROWSER_GENRE_PREFIX + genre.index) + mediaItems.add( + MediaBrowserCompat.MediaItem( + entryBuilder.build(), + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + ) + } + } + result.sendResult(mediaItems) + } + result.detach() + } +}