From dd65a12b53ea813f192553fc134913ec0b1f160a Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 4 Apr 2022 21:18:07 +0200 Subject: [PATCH] Migrate AutoMediaBrowser --- gradle/libs.versions.toml | 2 + ultrasonic/build.gradle | 1 + .../AutoMediaBrowserCallback.kt} | 804 +++++++++--------- .../ultrasonic/playback/CachedDataSource.kt | 8 +- .../playback/LegacyPlaylistManager.kt | 14 - .../ultrasonic/playback/MediaItemTree.kt | 247 ------ .../playback/MediaNotificationProvider.kt | 1 - .../ultrasonic/playback/PlaybackService.kt | 105 +-- .../moire/ultrasonic/service/Downloader.kt | 12 +- .../ultrasonic/service/JukeboxMediaPlayer.kt | 3 +- .../service/MediaPlayerLifecycleSupport.kt | 27 +- .../service/PlaybackStateSerializer.kt | 2 +- .../kotlin/org/moire/ultrasonic/util/Util.kt | 229 +---- 13 files changed, 465 insertions(+), 990 deletions(-) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{service/AutoMediaBrowserService.kt => playback/AutoMediaBrowserCallback.kt} (62%) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea4b36e8..59ea094a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,7 @@ multidex = "2.0.1" room = "2.4.0" kotlin = "1.6.10" kotlinxCoroutines = "1.6.0-native-mt" +kotlinxGuava = "1.6.0" viewModelKtx = "2.3.0" retrofit = "2.9.0" @@ -74,6 +75,7 @@ media3session = { module = "androidx.media3:media3-session", version.r kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +kotlinxGuava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxGuava"} retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } gsonConverter = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } jacksonConverter = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" } diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 57836a3b..3cfcdeb6 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -112,6 +112,7 @@ dependencies { implementation libs.kotlinStdlib implementation libs.kotlinxCoroutines + implementation libs.kotlinxGuava implementation libs.koinAndroid implementation libs.okhttpLogging implementation libs.fastScroll diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt similarity index 62% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index a8b3a9ce..c18c3193 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -1,32 +1,47 @@ /* - * AutoMediaBrowserService.kt - * Copyright (C) 2009-2021 Ultrasonic developers + * CustomMediaLibrarySessionCallback.kt + * Copyright (C) 2009-2022 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ -package org.moire.ultrasonic.service +package org.moire.ultrasonic.playback +import android.net.Uri import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.support.v4.media.MediaBrowserCompat -import android.support.v4.media.MediaDescriptionCompat -import androidx.media.MediaBrowserServiceCompat -import androidx.media.utils.MediaConstants -import io.reactivex.rxjava3.disposables.CompositeDisposable +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES +import androidx.media3.common.Player +import androidx.media3.session.LibraryResult +import androidx.media3.session.MediaLibraryService +import androidx.media3.session.MediaSession +import androidx.media3.session.SessionResult +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.guava.future import kotlinx.coroutines.launch -import org.koin.android.ext.android.inject +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.models.AlbumListType +import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Track +import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -66,13 +81,16 @@ private const val MEDIA_SEARCH_SONG_ITEM = "MEDIA_SEARCH_SONG_ITEM" private const val DISPLAY_LIMIT = 100 private const val SEARCH_LIMIT = 10 +private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch" +private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri" + /** * MediaBrowserService implementation for e.g. Android Auto */ @Suppress("TooManyFunctions", "LargeClass") -class AutoMediaBrowserService : MediaBrowserServiceCompat() { +class AutoMediaBrowserCallback(var player: Player) : + MediaLibraryService.MediaLibrarySession.MediaLibrarySessionCallback, KoinComponent { - private val lifecycleSupport by inject() private val mediaPlayerController by inject() private val activeServerProvider: ActiveServerProvider by inject() private val musicService = MusicServiceFactory.getMusicService() @@ -89,40 +107,200 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { private val useId3Tags get() = Settings.shouldUseId3Tags private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId - private var rxBusSubscription: CompositeDisposable = CompositeDisposable() - @Suppress("MagicNumber") - override fun onCreate() { - super.onCreate() - - rxBusSubscription += RxBus.mediaSessionTokenObservable.subscribe { - if (sessionToken == null) sessionToken = it - } - - rxBusSubscription += RxBus.playFromMediaIdCommandObservable.subscribe { - playFromMediaId(it.first) - } - - rxBusSubscription += RxBus.playFromSearchCommandObservable.subscribe { - playFromSearchCommand(it.first) - } - - val handler = Handler(Looper.getMainLooper()) - handler.postDelayed( - { - // Ultrasonic may be started from Android Auto. This boots up the necessary components. - Timber.d( - "AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService..." - ) - lifecycleSupport.onCreate() - DownloadService.getInstance() - }, - 100 + /** + * Called when a {@link MediaBrowser} requests the root {@link MediaItem} by {@link + * MediaBrowser#getLibraryRoot(LibraryParams)}. + * + *

Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser + * asynchronously. You can also return a {@link LibraryResult} directly by using Guava's + * {@link Futures#immediateFuture(Object)}. + * + *

The {@link LibraryResult#params} may differ from the given {@link LibraryParams params} + * if the session can't provide a root that matches with the {@code params}. + * + *

To allow browsing the media library, return a {@link LibraryResult} with {@link + * LibraryResult#RESULT_SUCCESS} and a root {@link MediaItem} with a valid {@link + * MediaItem#mediaId}. The media id is required for the browser to get the children under the + * root. + * + *

Interoperability: If this callback is called because a legacy {@link + * android.support.v4.media.MediaBrowserCompat} has requested a {@link + * androidx.media.MediaBrowserServiceCompat.BrowserRoot}, then the main thread may be blocked + * until the returned future is done. If your service may be queried by a legacy {@link + * android.support.v4.media.MediaBrowserCompat}, you should ensure that the future completes + * quickly to avoid blocking the main thread for a long period of time. + * + * @param session The session for this event. + * @param browser The browser information. + * @param params The optional parameters passed by the browser. + * @return A pending result that will be resolved with a root media item. + * @see SessionCommand#COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT + */ + override fun onGetLibraryRoot( + session: MediaLibraryService.MediaLibrarySession, + browser: MediaSession.ControllerInfo, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture> { + return Futures.immediateFuture( + LibraryResult.ofItem( + buildMediaItem( + "Root Folder", + MEDIA_ROOT_ID, + isPlayable = false, + folderType = FOLDER_TYPE_MIXED + ), + params + ) ) - - Timber.i("AutoMediaBrowserService onCreate finished") } + override fun onGetItem( + session: MediaLibraryService.MediaLibrarySession, + browser: MediaSession.ControllerInfo, + mediaId: String + ): ListenableFuture> { + playFromMediaId(mediaId) + + // TODO: Later + return Futures.immediateFuture( + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + ) + } + + override fun onGetChildren( + session: MediaLibraryService.MediaLibrarySession, + browser: MediaSession.ControllerInfo, + parentId: String, + page: Int, + pageSize: Int, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture>> { + // TODO: params??? + return onLoadChildren(parentId) + } + + private fun setMediaItemFromSearchQuery(query: String) { + // Only accept query with pattern "play [Title]" or "[Title]" + // Where [Title]: must be exactly matched + // If no media with exact name found, play a random media instead + val mediaTitle = + if (query.startsWith("play ", ignoreCase = true)) { + query.drop(5) + } else { + query + } + + playFromMediaId(mediaTitle) + } + + override fun onSetMediaUri( + session: MediaSession, + controller: MediaSession.ControllerInfo, + uri: Uri, + extras: Bundle + ): Int { + + if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) || + uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT) + ) { + val searchQuery = + uri.getQueryParameter("query") + ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED + setMediaItemFromSearchQuery(searchQuery) + + return SessionResult.RESULT_SUCCESS + } else { + return SessionResult.RESULT_ERROR_NOT_SUPPORTED + } + } + + + @Suppress("ReturnCount", "ComplexMethod") + fun onLoadChildren( + parentId: String, + ): ListenableFuture>> { + Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId) + + val parentIdParts = parentId.split('|') + + when (parentIdParts.first()) { + MEDIA_ROOT_ID -> return getRootItems() + MEDIA_LIBRARY_ID -> return getLibrary() + MEDIA_ARTIST_ID -> return getArtists() + MEDIA_ARTIST_SECTION -> return getArtists(parentIdParts[1]) + MEDIA_ALBUM_ID -> return getAlbums(AlbumListType.SORTED_BY_NAME) + MEDIA_ALBUM_PAGE_ID -> return getAlbums( + AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt() + ) + MEDIA_PLAYLIST_ID -> return getPlaylists() + MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(AlbumListType.FREQUENT) + MEDIA_ALBUM_NEWEST_ID -> return getAlbums(AlbumListType.NEWEST) + MEDIA_ALBUM_RECENT_ID -> return getAlbums(AlbumListType.RECENT) + MEDIA_ALBUM_RANDOM_ID -> return getAlbums(AlbumListType.RANDOM) + MEDIA_ALBUM_STARRED_ID -> return getAlbums(AlbumListType.STARRED) + MEDIA_SONG_RANDOM_ID -> return getRandomSongs() + MEDIA_SONG_STARRED_ID -> return getStarredSongs() + MEDIA_SHARE_ID -> return getShares() + MEDIA_BOOKMARK_ID -> return getBookmarks() + MEDIA_PODCAST_ID -> return getPodcasts() + MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2]) + MEDIA_ARTIST_ITEM -> return getAlbumsForArtist( + parentIdParts[1], parentIdParts[2] + ) + MEDIA_ALBUM_ITEM -> return getSongsForAlbum(parentIdParts[1], parentIdParts[2]) + MEDIA_SHARE_ITEM -> return getSongsForShare(parentIdParts[1]) + MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(parentIdParts[1]) + else -> return Futures.immediateFuture(LibraryResult.ofItemList(listOf(), null)) + } + } + + fun onSearch( + query: String, + extras: Bundle?, + ): ListenableFuture>> { + Timber.d("AutoMediaBrowserService onSearch query: %s", query) + val mediaItems: MutableList = ArrayList() + + return serviceScope.future { + val criteria = SearchCriteria(query, SEARCH_LIMIT, SEARCH_LIMIT, SEARCH_LIMIT) + val searchResult = callWithErrorHandling { musicService.search(criteria) } + + // TODO Add More... button to categories + if (searchResult != null) { + searchResult.artists.map { artist -> + mediaItems.add( + artist.name ?: "", + listOf(MEDIA_ARTIST_ITEM, artist.id, artist.name).joinToString("|"), + FOLDER_TYPE_ARTISTS + ) + } + + searchResult.albums.map { album -> + mediaItems.add( + album.title ?: "", + listOf(MEDIA_ALBUM_ITEM, album.id, album.name) + .joinToString("|"), + FOLDER_TYPE_ALBUMS + ) + } + + searchSongsCache = searchResult.songs + searchResult.songs.map { song -> + mediaItems.add( + buildMediaItemFromTrack( + song, + listOf(MEDIA_SEARCH_SONG_ITEM, song.id).joinToString("|"), + isPlayable = true + ) + ) + } + } + return@future LibraryResult.ofItemList(mediaItems, null) + } + } + + @Suppress("MagicNumber", "ComplexMethod") private fun playFromMediaId(mediaId: String?) { Timber.d( @@ -180,132 +358,6 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - override fun onDestroy() { - super.onDestroy() - rxBusSubscription.dispose() - serviceJob.cancel() - - Timber.i("AutoMediaBrowserService onDestroy finished") - } - - override fun onGetRoot( - clientPackageName: String, - clientUid: Int, - rootHints: Bundle? - ): BrowserRoot { - Timber.d( - "AutoMediaBrowserService onGetRoot called. clientPackageName: %s; clientUid: %d", - clientPackageName, clientUid - ) - - 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 - ) - extras.putBoolean( - MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true - ) - - return BrowserRoot(MEDIA_ROOT_ID, extras) - } - - @Suppress("ReturnCount", "ComplexMethod") - override fun onLoadChildren( - parentId: String, - result: Result> - ) { - Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId) - - val parentIdParts = parentId.split('|') - - when (parentIdParts.first()) { - MEDIA_ROOT_ID -> return getRootItems(result) - MEDIA_LIBRARY_ID -> return getLibrary(result) - MEDIA_ARTIST_ID -> return getArtists(result) - MEDIA_ARTIST_SECTION -> return getArtists(result, parentIdParts[1]) - MEDIA_ALBUM_ID -> return getAlbums(result, AlbumListType.SORTED_BY_NAME) - MEDIA_ALBUM_PAGE_ID -> return getAlbums( - result, AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt() - ) - MEDIA_PLAYLIST_ID -> return getPlaylists(result) - MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(result, AlbumListType.FREQUENT) - MEDIA_ALBUM_NEWEST_ID -> return getAlbums(result, AlbumListType.NEWEST) - MEDIA_ALBUM_RECENT_ID -> return getAlbums(result, AlbumListType.RECENT) - MEDIA_ALBUM_RANDOM_ID -> return getAlbums(result, AlbumListType.RANDOM) - MEDIA_ALBUM_STARRED_ID -> return getAlbums(result, AlbumListType.STARRED) - MEDIA_SONG_RANDOM_ID -> return getRandomSongs(result) - MEDIA_SONG_STARRED_ID -> return getStarredSongs(result) - MEDIA_SHARE_ID -> return getShares(result) - MEDIA_BOOKMARK_ID -> return getBookmarks(result) - MEDIA_PODCAST_ID -> return getPodcasts(result) - MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2], result) - MEDIA_ARTIST_ITEM -> return getAlbumsForArtist( - result, parentIdParts[1], parentIdParts[2] - ) - MEDIA_ALBUM_ITEM -> return getSongsForAlbum(result, parentIdParts[1], parentIdParts[2]) - MEDIA_SHARE_ITEM -> return getSongsForShare(result, parentIdParts[1]) - MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(result, parentIdParts[1]) - else -> result.sendResult(mutableListOf()) - } - } - - override fun onSearch( - query: String, - extras: Bundle?, - result: Result> - ) { - Timber.d("AutoMediaBrowserService onSearch query: %s", query) - val mediaItems: MutableList = ArrayList() - result.detach() - - serviceScope.launch { - val criteria = SearchCriteria(query, SEARCH_LIMIT, SEARCH_LIMIT, SEARCH_LIMIT) - val searchResult = callWithErrorHandling { musicService.search(criteria) } - - // TODO Add More... button to categories - if (searchResult != null) { - searchResult.artists.map { artist -> - mediaItems.add( - artist.name ?: "", - listOf(MEDIA_ARTIST_ITEM, artist.id, artist.name).joinToString("|"), - null, - R.string.search_artists - ) - } - - searchResult.albums.map { album -> - mediaItems.add( - album.title ?: "", - listOf(MEDIA_ALBUM_ITEM, album.id, album.name) - .joinToString("|"), - null, - R.string.search_albums - ) - } - - searchSongsCache = searchResult.songs - searchResult.songs.map { song -> - mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - song, - listOf(MEDIA_SEARCH_SONG_ITEM, song.id).joinToString("|"), - R.string.search_songs - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE - ) - ) - } - } - result.sendResult(mediaItems) - } - } - private fun playSearch(id: String) { serviceScope.launch { // If there is no cache, we can't play the selected song. @@ -316,112 +368,108 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun getRootItems(result: Result>) { - val mediaItems: MutableList = ArrayList() + private fun getRootItems(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() if (!isOffline) mediaItems.add( R.string.music_library_label, MEDIA_LIBRARY_ID, - R.drawable.ic_library, null ) mediaItems.add( R.string.main_artists_title, MEDIA_ARTIST_ID, - R.drawable.ic_artist, - null + null, + folderType = FOLDER_TYPE_ARTISTS ) if (!isOffline) mediaItems.add( R.string.main_albums_title, MEDIA_ALBUM_ID, - R.drawable.ic_menu_browse_dark, - null + null, + folderType = FOLDER_TYPE_ALBUMS ) mediaItems.add( R.string.playlist_label, MEDIA_PLAYLIST_ID, - R.drawable.ic_menu_playlists_dark, - null + null, + folderType = FOLDER_TYPE_PLAYLISTS ) - result.sendResult(mediaItems) + return Futures.immediateFuture(LibraryResult.ofItemList(mediaItems, null)) } - private fun getLibrary(result: Result>) { - val mediaItems: MutableList = ArrayList() + private fun getLibrary(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() // Songs mediaItems.add( R.string.main_songs_random, MEDIA_SONG_RANDOM_ID, - null, - R.string.main_songs_title + R.string.main_songs_title, + folderType = FOLDER_TYPE_TITLES ) mediaItems.add( R.string.main_songs_starred, MEDIA_SONG_STARRED_ID, - null, - R.string.main_songs_title + R.string.main_songs_title, + folderType = FOLDER_TYPE_TITLES ) // Albums mediaItems.add( R.string.main_albums_newest, MEDIA_ALBUM_NEWEST_ID, - null, R.string.main_albums_title ) mediaItems.add( R.string.main_albums_recent, MEDIA_ALBUM_RECENT_ID, - null, - R.string.main_albums_title + R.string.main_albums_title, + folderType = FOLDER_TYPE_ALBUMS ) mediaItems.add( R.string.main_albums_frequent, MEDIA_ALBUM_FREQUENT_ID, - null, - R.string.main_albums_title + R.string.main_albums_title, + folderType = FOLDER_TYPE_ALBUMS ) mediaItems.add( R.string.main_albums_random, MEDIA_ALBUM_RANDOM_ID, - null, - R.string.main_albums_title + R.string.main_albums_title, + folderType = FOLDER_TYPE_ALBUMS ) mediaItems.add( R.string.main_albums_starred, MEDIA_ALBUM_STARRED_ID, - null, - R.string.main_albums_title + R.string.main_albums_title, + folderType = FOLDER_TYPE_ALBUMS ) // Other - mediaItems.add(R.string.button_bar_shares, MEDIA_SHARE_ID, null, null) - mediaItems.add(R.string.button_bar_bookmarks, MEDIA_BOOKMARK_ID, null, null) - mediaItems.add(R.string.button_bar_podcasts, MEDIA_PODCAST_ID, null, null) + mediaItems.add(R.string.button_bar_shares, MEDIA_SHARE_ID, null) + mediaItems.add(R.string.button_bar_bookmarks, MEDIA_BOOKMARK_ID, null) + mediaItems.add(R.string.button_bar_podcasts, MEDIA_PODCAST_ID, null) - result.sendResult(mediaItems) + return Futures.immediateFuture(LibraryResult.ofItemList(mediaItems, null)) } private fun getArtists( - result: Result>, section: String? = null - ) { - val mediaItems: MutableList = ArrayList() - result.detach() + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val childMediaId: String var artists = if (!isOffline && useId3Tags) { childMediaId = MEDIA_ARTIST_ITEM @@ -452,7 +500,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { mediaItems.add( currentSection, listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|"), - null + FOLDER_TYPE_ARTISTS ) } } @@ -461,23 +509,22 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { mediaItems.add( artist.name ?: "", listOf(childMediaId, artist.id, artist.name).joinToString("|"), - null + FOLDER_TYPE_ARTISTS ) } } - result.sendResult(mediaItems) } + return@future LibraryResult.ofItemList(mediaItems, null) } } private fun getAlbumsForArtist( - result: Result>, id: String, name: String - ) { - val mediaItems: MutableList = ArrayList() - result.detach() - serviceScope.launch { + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() + + return serviceScope.future { val albums = if (!isOffline && useId3Tags) { callWithErrorHandling { musicService.getArtist(id, name, false) } } else { @@ -491,22 +538,20 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) .joinToString("|"), - null + FOLDER_TYPE_ALBUMS ) } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } private fun getSongsForAlbum( - result: Result>, id: String, name: String - ) { - val mediaItems: MutableList = ArrayList() - result.detach() + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val songs = listSongsInMusicService(id, name) if (songs != null) { @@ -520,43 +565,36 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { items.map { item -> if (item.isDirectory) mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - item, - listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_BROWSABLE - ) + item.title ?: "", + listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|"), + FOLDER_TYPE_TITLES ) else mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - item, - listOf( - MEDIA_ALBUM_SONG_ITEM, - id, - name, - item.id - ).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + item, + listOf( + MEDIA_ALBUM_SONG_ITEM, + id, + name, + item.id + ).joinToString("|"), + isPlayable = true ) ) } } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } private fun getAlbums( - result: Result>, type: AlbumListType, page: Int? = null - ) { - val mediaItems: MutableList = ArrayList() - result.detach() - serviceScope.launch { + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() + + return serviceScope.future { val offset = (page ?: 0) * DISPLAY_LIMIT val albums = if (useId3Tags) { callWithErrorHandling { @@ -577,7 +615,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) .joinToString("|"), - null + FOLDER_TYPE_ALBUMS ) } @@ -585,41 +623,37 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { mediaItems.add( R.string.search_more, listOf(MEDIA_ALBUM_PAGE_ID, type.typeName, (page ?: 0) + 1).joinToString("|"), - R.drawable.ic_menu_forward_dark, null ) - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } - private fun getPlaylists(result: Result>) { - val mediaItems: MutableList = ArrayList() - result.detach() + private fun getPlaylists(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val playlists = callWithErrorHandling { musicService.getPlaylists(true) } playlists?.map { playlist -> mediaItems.add( playlist.name, listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name) .joinToString("|"), - null + FOLDER_TYPE_PLAYLISTS ) } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } private fun getPlaylist( id: String, name: String, - result: Result> - ) { - val mediaItems: MutableList = ArrayList() - result.detach() + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val content = callWithErrorHandling { musicService.getPlaylist(id, name) } if (content != null) { @@ -632,22 +666,20 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { playlistCache = content.getTracks() playlistCache!!.take(DISPLAY_LIMIT).map { item -> mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - item, - listOf( - MEDIA_PLAYLIST_SONG_ITEM, - id, - name, - item.id - ).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + item, + listOf( + MEDIA_PLAYLIST_SONG_ITEM, + id, + name, + item.id + ).joinToString("|"), + isPlayable = true ) ) } - result.sendResult(mediaItems) } + return@future LibraryResult.ofItemList(mediaItems, null) } } @@ -689,30 +721,28 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun getPodcasts(result: Result>) { - val mediaItems: MutableList = ArrayList() - result.detach() - serviceScope.launch { + private fun getPodcasts(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() + + return serviceScope.future { val podcasts = callWithErrorHandling { musicService.getPodcastsChannels(false) } podcasts?.map { podcast -> mediaItems.add( podcast.title ?: "", listOf(MEDIA_PODCAST_ITEM, podcast.id).joinToString("|"), - null + FOLDER_TYPE_MIXED ) } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } private fun getPodcastEpisodes( - result: Result>, id: String - ) { - val mediaItems: MutableList = ArrayList() - result.detach() - serviceScope.launch { + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() + return serviceScope.future { val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) } if (episodes != null) { @@ -721,18 +751,16 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { episodes.getTracks().map { episode -> mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - episode, - listOf(MEDIA_PODCAST_EPISODE_ITEM, id, episode.id) - .joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + episode, + listOf(MEDIA_PODCAST_EPISODE_ITEM, id, episode.id) + .joinToString("|"), + isPlayable = true ) ) } - result.sendResult(mediaItems) } + return@future LibraryResult.ofItemList(mediaItems, null) } } @@ -757,27 +785,24 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun getBookmarks(result: Result>) { - val mediaItems: MutableList = ArrayList() - result.detach() - serviceScope.launch { + private fun getBookmarks(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() + return serviceScope.future { val bookmarks = callWithErrorHandling { musicService.getBookmarks() } if (bookmarks != null) { val songs = Util.getSongsFromBookmarks(bookmarks) songs.getTracks().map { song -> mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - song, - listOf(MEDIA_BOOKMARK_ITEM, song.id).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + song, + listOf(MEDIA_BOOKMARK_ITEM, song.id).joinToString("|"), + isPlayable = true ) ) } - result.sendResult(mediaItems) } + return@future LibraryResult.ofItemList(mediaItems, null) } } @@ -792,11 +817,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun getShares(result: Result>) { - val mediaItems: MutableList = ArrayList() - result.detach() + private fun getShares(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val shares = callWithErrorHandling { musicService.getShares(false) } shares?.map { share -> @@ -804,21 +828,19 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { share.name ?: "", listOf(MEDIA_SHARE_ITEM, share.id) .joinToString("|"), - null + FOLDER_TYPE_MIXED ) } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } private fun getSongsForShare( - result: Result>, id: String - ) { - val mediaItems: MutableList = ArrayList() - result.detach() + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val shares = callWithErrorHandling { musicService.getShares(false) } val selectedShare = shares?.firstOrNull { share -> share.id == id } @@ -829,17 +851,15 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { selectedShare.getEntries().map { song -> mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - song, - listOf(MEDIA_SHARE_SONG_ITEM, id, song.id).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + song, + listOf(MEDIA_SHARE_SONG_ITEM, id, song.id).joinToString("|"), + isPlayable = true ) ) } } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } @@ -864,11 +884,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun getStarredSongs(result: Result>) { - val mediaItems: MutableList = ArrayList() - result.detach() + private fun getStarredSongs(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val songs = listStarredSongsInMusicService() if (songs != null) { @@ -880,17 +899,15 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { starredSongsCache = items items.map { song -> mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - song, - listOf(MEDIA_SONG_STARRED_ITEM, song.id).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + song, + listOf(MEDIA_SONG_STARRED_ITEM, song.id).joinToString("|"), + isPlayable = true ) ) } } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } @@ -917,11 +934,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun getRandomSongs(result: Result>) { - val mediaItems: MutableList = ArrayList() - result.detach() + private fun getRandomSongs(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val songs = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) } if (songs != null) { @@ -933,17 +949,15 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { randomSongsCache = items items.map { song -> mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - song, - listOf(MEDIA_SONG_RANDOM_ITEM, song.id).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + song, + listOf(MEDIA_SONG_RANDOM_ITEM, song.id).joinToString("|"), + isPlayable = true ) ) } } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } @@ -985,77 +999,47 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun MutableList.add( + private fun MutableList.add( title: String, mediaId: String, - icon: Int?, - groupNameId: Int? = null + folderType: Int ) { - val builder = MediaDescriptionCompat.Builder() - builder.setTitle(title) - builder.setMediaId(mediaId) - if (icon != null) - builder.setIconUri(Util.getUriToDrawable(applicationContext, icon)) - - if (groupNameId != null) - builder.setExtras( - Bundle().apply { - putString( - MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, - getString(groupNameId) - ) - } - ) - - val mediaItem = MediaBrowserCompat.MediaItem( - builder.build(), - MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + val mediaItem = buildMediaItem( + title, + mediaId, + isPlayable = false, + folderType = folderType ) this.add(mediaItem) } - private fun MutableList.add( + private fun MutableList.add( resId: Int, mediaId: String, - icon: Int?, groupNameId: Int?, - browsable: Boolean = true + browsable: Boolean = true, + folderType: Int = FOLDER_TYPE_MIXED ) { - val builder = MediaDescriptionCompat.Builder() - builder.setTitle(getString(resId)) - builder.setMediaId(mediaId) + val applicationContext = UApp.applicationContext() - if (icon != null) - builder.setIconUri(Util.getUriToDrawable(applicationContext, icon)) - - if (groupNameId != null) - builder.setExtras( - Bundle().apply { - putString( - MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, - getString(groupNameId) - ) - } - ) - - val mediaItem = MediaBrowserCompat.MediaItem( - builder.build(), - if (browsable) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE - else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + val mediaItem = buildMediaItem( + applicationContext.getString(resId), + mediaId, + isPlayable = false, + folderType = folderType ) this.add(mediaItem) } - private fun MutableList.addPlayAllItem( + private fun MutableList.addPlayAllItem( mediaId: String, ) { this.add( R.string.select_album_play_all, mediaId, - R.drawable.ic_stat_play_dark, null, false ) @@ -1098,4 +1082,52 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { null } } -} + + + private fun buildMediaItemFromTrack( + track: Track, + mediaId: String, + isPlayable: Boolean + ): MediaItem { + + return buildMediaItem( + title = track.title ?: "", + mediaId = mediaId, + isPlayable = isPlayable, + folderType = FOLDER_TYPE_NONE, + album = track.album, + artist = track.artist, + genre = track.genre, + ) + } + + @Suppress("LongParameterList") + private fun buildMediaItem( + title: String, + mediaId: String, + isPlayable: Boolean, + @MediaMetadata.FolderType folderType: Int, + album: String? = null, + artist: String? = null, + genre: String? = null, + sourceUri: Uri? = null, + imageUri: Uri? = null, + ): MediaItem { + val metadata = + MediaMetadata.Builder() + .setAlbumTitle(album) + .setTitle(title) + .setArtist(artist) + .setGenre(genre) + .setFolderType(folderType) + .setIsPlayable(isPlayable) + .setArtworkUri(imageUri) + .build() + return MediaItem.Builder() + .setMediaId(mediaId) + .setMediaMetadata(metadata) + .setUri(sourceUri) + .build() + } + +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt index 8de2a107..4c42a441 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt @@ -30,7 +30,7 @@ class CachedDataSource( ) : BaseDataSource(false) { class Factory( - var upstreamDataSourceFactory: DataSource.Factory + private var upstreamDataSourceFactory: DataSource.Factory ) : DataSource.Factory { private var eventListener: EventListener? = null @@ -112,16 +112,16 @@ class CachedDataSource( } override fun read(buffer: ByteArray, offset: Int, length: Int): Int { - if (cachePath != null) { + return if (cachePath != null) { try { - return readInternal(buffer, offset, length) + readInternal(buffer, offset, length) } catch (e: IOException) { throw HttpDataSource.HttpDataSourceException.createForIOException( e, Util.castNonNull(dataSpec), HttpDataSource.HttpDataSourceException.TYPE_READ ) } } else { - return upstreamDataSource.read(buffer, offset, length) + upstreamDataSource.read(buffer, offset, length) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt index 86a0483a..0afaccd2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt @@ -64,20 +64,6 @@ class LegacyPlaylistManager : KoinComponent { currentPlaying = mediaItemCache[item?.mediaMetadata?.mediaUri.toString()] } - @Synchronized - fun clearIncomplete() { - val iterator = _playlist.iterator() - var changedPlaylist = false - while (iterator.hasNext()) { - val downloadFile = iterator.next() - if (!downloadFile.isCompleteFileAvailable) { - iterator.remove() - changedPlaylist = true - } - } - if (changedPlaylist) playlistUpdateRevision++ - } - @Synchronized fun clearPlaylist() { _playlist.clear() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt deleted file mode 100644 index d2f76e5f..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.moire.ultrasonic.playback - -import android.content.res.AssetManager -import android.net.Uri -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS -import com.google.common.collect.ImmutableList -import org.json.JSONObject - -/** - * A sample media catalog that represents media items as a tree. - * - * It fetched the data from {@code catalog.json}. The root's children are folders containing media - * items from the same album/artist/genre. - * - * Each app should have their own way of representing the tree. MediaItemTree is used for - * demonstration purpose only. - */ -object MediaItemTree { - private var treeNodes: MutableMap = mutableMapOf() - private var titleMap: MutableMap = mutableMapOf() - private var isInitialized = false - private const val ROOT_ID = "[rootID]" - private const val ALBUM_ID = "[albumID]" - private const val GENRE_ID = "[genreID]" - private const val ARTIST_ID = "[artistID]" - private const val ALBUM_PREFIX = "[album]" - private const val GENRE_PREFIX = "[genre]" - private const val ARTIST_PREFIX = "[artist]" - private const val ITEM_PREFIX = "[item]" - - private class MediaItemNode(val item: MediaItem) { - private val children: MutableList = ArrayList() - - fun addChild(childID: String) { - this.children.add(treeNodes[childID]!!.item) - } - - fun getChildren(): List { - return ImmutableList.copyOf(children) - } - } - - private fun buildMediaItem( - title: String, - mediaId: String, - isPlayable: Boolean, - @MediaMetadata.FolderType folderType: Int, - album: String? = null, - artist: String? = null, - genre: String? = null, - sourceUri: Uri? = null, - imageUri: Uri? = null, - ): MediaItem { - // TODO(b/194280027): add artwork - val metadata = - MediaMetadata.Builder() - .setAlbumTitle(album) - .setTitle(title) - .setArtist(artist) - .setGenre(genre) - .setFolderType(folderType) - .setIsPlayable(isPlayable) - .setArtworkUri(imageUri) - .build() - return MediaItem.Builder() - .setMediaId(mediaId) - .setMediaMetadata(metadata) - .setUri(sourceUri) - .build() - } - - fun initialize(assets: AssetManager) { - if (isInitialized) return - isInitialized = true - // create root and folders for album/artist/genre. - treeNodes[ROOT_ID] = - MediaItemNode( - buildMediaItem( - title = "Root Folder", - mediaId = ROOT_ID, - isPlayable = false, - folderType = FOLDER_TYPE_MIXED - ) - ) - treeNodes[ALBUM_ID] = - MediaItemNode( - buildMediaItem( - title = "Album Folder", - mediaId = ALBUM_ID, - isPlayable = false, - folderType = FOLDER_TYPE_MIXED - ) - ) - treeNodes[ARTIST_ID] = - MediaItemNode( - buildMediaItem( - title = "Artist Folder", - mediaId = ARTIST_ID, - isPlayable = false, - folderType = FOLDER_TYPE_MIXED - ) - ) - treeNodes[GENRE_ID] = - MediaItemNode( - buildMediaItem( - title = "Genre Folder", - mediaId = GENRE_ID, - isPlayable = false, - folderType = FOLDER_TYPE_MIXED - ) - ) - treeNodes[ROOT_ID]!!.addChild(ALBUM_ID) - treeNodes[ROOT_ID]!!.addChild(ARTIST_ID) - treeNodes[ROOT_ID]!!.addChild(GENRE_ID) - - // Here, parse the json file in asset for media list. - // We use a file in asset for demo purpose -// val jsonObject = JSONObject(loadJSONFromAsset(assets)) -// val mediaList = jsonObject.getJSONArray("media") -// -// // create subfolder with same artist, album, etc. -// for (i in 0 until mediaList.length()) { -// addNodeToTree(mediaList.getJSONObject(i)) -// } - } - - private fun addNodeToTree(mediaObject: JSONObject) { - - val id = mediaObject.getString("id") - val album = mediaObject.getString("album") - val title = mediaObject.getString("title") - val artist = mediaObject.getString("artist") - val genre = mediaObject.getString("genre") - val sourceUri = Uri.parse(mediaObject.getString("source")) - val imageUri = Uri.parse(mediaObject.getString("image")) - // key of such items in tree - val idInTree = ITEM_PREFIX + id - val albumFolderIdInTree = ALBUM_PREFIX + album - val artistFolderIdInTree = ARTIST_PREFIX + artist - val genreFolderIdInTree = GENRE_PREFIX + genre - - treeNodes[idInTree] = - MediaItemNode( - buildMediaItem( - title = title, - mediaId = idInTree, - isPlayable = true, - album = album, - artist = artist, - genre = genre, - sourceUri = sourceUri, - imageUri = imageUri, - folderType = FOLDER_TYPE_NONE - ) - ) - - titleMap[title.lowercase()] = treeNodes[idInTree]!! - - if (!treeNodes.containsKey(albumFolderIdInTree)) { - treeNodes[albumFolderIdInTree] = - MediaItemNode( - buildMediaItem( - title = album, - mediaId = albumFolderIdInTree, - isPlayable = true, - folderType = FOLDER_TYPE_PLAYLISTS - ) - ) - treeNodes[ALBUM_ID]!!.addChild(albumFolderIdInTree) - } - treeNodes[albumFolderIdInTree]!!.addChild(idInTree) - - // add into artist folder - if (!treeNodes.containsKey(artistFolderIdInTree)) { - treeNodes[artistFolderIdInTree] = - MediaItemNode( - buildMediaItem( - title = artist, - mediaId = artistFolderIdInTree, - isPlayable = true, - folderType = FOLDER_TYPE_PLAYLISTS - ) - ) - treeNodes[ARTIST_ID]!!.addChild(artistFolderIdInTree) - } - treeNodes[artistFolderIdInTree]!!.addChild(idInTree) - - // add into genre folder - if (!treeNodes.containsKey(genreFolderIdInTree)) { - treeNodes[genreFolderIdInTree] = - MediaItemNode( - buildMediaItem( - title = genre, - mediaId = genreFolderIdInTree, - isPlayable = true, - folderType = FOLDER_TYPE_PLAYLISTS - ) - ) - treeNodes[GENRE_ID]!!.addChild(genreFolderIdInTree) - } - treeNodes[genreFolderIdInTree]!!.addChild(idInTree) - } - - fun getItem(id: String): MediaItem? { - return treeNodes[id]?.item - } - - fun getRootItem(): MediaItem { - return treeNodes[ROOT_ID]!!.item - } - - fun getChildren(id: String): List? { - return treeNodes[id]?.getChildren() - } - - fun getRandomItem(): MediaItem { - var curRoot = getRootItem() - while (curRoot.mediaMetadata.folderType != FOLDER_TYPE_NONE) { - val children = getChildren(curRoot.mediaId)!! - curRoot = children.random() - } - return curRoot - } - - fun getItemFromTitle(title: String): MediaItem? { - return titleMap[title]?.item - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt index 2eaaba9a..8ef86f09 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt @@ -50,7 +50,6 @@ internal class MediaNotificationProvider(context: Context) : context, NOTIFICATION_CHANNEL_ID ) - // TODO(b/193193926): Filter actions depending on the player's available commands. // Skip to previous action. builder.addAction( actionFactory.createMediaAction( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index bfad720a..2b3ad224 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -18,8 +18,6 @@ package org.moire.ultrasonic.playback import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent -import android.net.Uri -import android.os.Bundle import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.C.CONTENT_TYPE_MUSIC @@ -29,13 +27,8 @@ import androidx.media3.datasource.DataSource import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory -import androidx.media3.session.LibraryResult import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession -import androidx.media3.session.SessionResult -import com.google.common.collect.ImmutableList -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.activity.NavigationActivity @@ -48,94 +41,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent { private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var dataSourceFactory: DataSource.Factory - private val librarySessionCallback = CustomMediaLibrarySessionCallback() - - companion object { - private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch" - private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri" - } - - private inner class CustomMediaLibrarySessionCallback : - MediaLibrarySession.MediaLibrarySessionCallback { - override fun onGetLibraryRoot( - session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, - params: LibraryParams? - ): ListenableFuture> { - return Futures.immediateFuture( - LibraryResult.ofItem( - MediaItemTree.getRootItem(), - params - ) - ) - } - - override fun onGetItem( - session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, - mediaId: String - ): ListenableFuture> { - val item = - MediaItemTree.getItem(mediaId) - ?: return Futures.immediateFuture( - LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) - ) - return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null)) - } - - override fun onGetChildren( - session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, - parentId: String, - page: Int, - pageSize: Int, - params: LibraryParams? - ): ListenableFuture>> { - val children = - MediaItemTree.getChildren(parentId) - ?: return Futures.immediateFuture( - LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) - ) - - return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) - } - - private fun setMediaItemFromSearchQuery(query: String) { - // Only accept query with pattern "play [Title]" or "[Title]" - // Where [Title]: must be exactly matched - // If no media with exact name found, play a random media instead - val mediaTitle = - if (query.startsWith("play ", ignoreCase = true)) { - query.drop(5) - } else { - query - } - - val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem() - player.setMediaItem(item) - } - - override fun onSetMediaUri( - session: MediaSession, - controller: MediaSession.ControllerInfo, - uri: Uri, - extras: Bundle - ): Int { - - if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) || - uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT) - ) { - val searchQuery = - uri.getQueryParameter("query") - ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED - setMediaItemFromSearchQuery(searchQuery) - - return SessionResult.RESULT_SUCCESS - } else { - return SessionResult.RESULT_ERROR_NOT_SUPPORTED - } - } - } + private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback /* * For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer, @@ -148,11 +54,9 @@ class PlaybackService : MediaLibraryService(), KoinComponent { mediaItem: MediaItem ): MediaItem { // Again, set the Uri, so that it will get a LocalConfiguration - val item = mediaItem.buildUpon() + return mediaItem.buildUpon() .setUri(mediaItem.mediaMetadata.mediaUri) .build() - - return item } } @@ -202,9 +106,10 @@ class PlaybackService : MediaLibraryService(), KoinComponent { // Enable audio offload player.experimentalSetOffloadSchedulingEnabled(true) - MediaItemTree.initialize(assets) + // Create browser interface + librarySessionCallback = AutoMediaBrowserCallback(player) - // THIS Will need to use the AutoCalls + // This will need to use the AutoCalls mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) .setMediaItemFiller(CustomMediaItemFiller()) .setSessionActivity(getPendingIntentForContent()) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 3cb70bd6..4cb69b04 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -64,7 +64,7 @@ class Downloader( private val rxBusSubscription: CompositeDisposable = CompositeDisposable() - var downloadChecker = object : Runnable { + private var downloadChecker = object : Runnable { override fun run() { try { Timber.w("Checking Downloads") @@ -399,11 +399,11 @@ class Downloader( val fileLength = Storage.getFromPath(downloadFile.partialFile)?.length ?: 0 needsDownloading = ( - downloadFile.desiredBitRate == 0 || - duration == null || - duration == 0 || - fileLength == 0L - ) + downloadFile.desiredBitRate == 0 || + duration == null || + duration == 0 || + fileLength == 0L + ) if (needsDownloading) { // Attempt partial HTTP GET, appending to the file if it exists. diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt index a6326cd3..75c926a1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt @@ -8,6 +8,7 @@ package org.moire.ultrasonic.service import android.content.Context import android.os.Handler +import android.os.Looper import android.view.Gravity import android.view.LayoutInflater import android.view.View @@ -145,7 +146,7 @@ class JukeboxMediaPlayer(private val downloader: Downloader) { private fun disableJukeboxOnError(x: Throwable, resourceId: Int) { Timber.w(x.toString()) val context = applicationContext() - Handler().post { toast(context, resourceId, false) } + Handler(Looper.getMainLooper()).post { toast(context, resourceId, false) } mediaPlayerControllerLazy.value.isJukeboxEnabled = false } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 77b55348..c41d6c85 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -66,12 +66,11 @@ class MediaPlayerLifecycleSupport : KoinComponent { if (!created) return - // TODO -// playbackStateSerializer.serializeNow( -// downloader.getPlaylist(), -// downloader.currentPlayingIndex, -// mediaPlayerController.playerPosition -// ) + playbackStateSerializer.serializeNow( + mediaPlayerController.playList, + mediaPlayerController.currentMediaItemIndex, + mediaPlayerController.playerPosition + ) mediaPlayerController.clear(false) mediaButtonEventSubscription?.dispose() @@ -110,10 +109,10 @@ class MediaPlayerLifecycleSupport : KoinComponent { val autoStart = keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || - keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || - keyCode == KeyEvent.KEYCODE_HEADSETHOOK || - keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || - keyCode == KeyEvent.KEYCODE_MEDIA_NEXT + keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || + keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || + keyCode == KeyEvent.KEYCODE_MEDIA_NEXT // We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start onCreate(autoStart) { @@ -150,10 +149,10 @@ class MediaPlayerLifecycleSupport : KoinComponent { return val autoStart = action == Constants.CMD_PLAY || - action == Constants.CMD_RESUME_OR_PLAY || - action == Constants.CMD_TOGGLEPAUSE || - action == Constants.CMD_PREVIOUS || - action == Constants.CMD_NEXT + action == Constants.CMD_RESUME_OR_PLAY || + action == Constants.CMD_TOGGLEPAUSE || + action == Constants.CMD_PREVIOUS || + action == Constants.CMD_NEXT // We can receive intents when everything is stopped, so we need to start onCreate(autoStart) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt index edb1cea5..7115140a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt @@ -53,7 +53,7 @@ class PlaybackStateSerializer : KoinComponent { } } - private fun serializeNow( + fun serializeNow( songs: Iterable, currentPlayingIndex: Int, currentPlayingPosition: Int diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index febda154..109b8163 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -9,10 +9,8 @@ package org.moire.ultrasonic.util import android.annotation.SuppressLint import android.app.Activity -import android.app.PendingIntent import android.content.ContentResolver import android.content.Context -import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -28,18 +26,20 @@ import android.net.Uri import android.net.wifi.WifiManager import android.net.wifi.WifiManager.WifiLock import android.os.Build -import android.os.Bundle import android.os.Environment -import android.os.Parcelable -import android.support.v4.media.MediaDescriptionCompat import android.text.TextUtils import android.util.TypedValue import android.view.Gravity -import android.view.KeyEvent import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.annotation.AnyRes -import androidx.media.utils.MediaConstants +import org.moire.ultrasonic.R +import org.moire.ultrasonic.app.UApp.Companion.applicationContext +import org.moire.ultrasonic.domain.Bookmark +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.SearchResult +import org.moire.ultrasonic.domain.Track +import timber.log.Timber import java.io.Closeable import java.io.UnsupportedEncodingException import java.security.MessageDigest @@ -49,15 +49,6 @@ import java.util.concurrent.TimeUnit import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt -import org.moire.ultrasonic.R -import org.moire.ultrasonic.app.UApp.Companion.applicationContext -import org.moire.ultrasonic.domain.Bookmark -import org.moire.ultrasonic.domain.MusicDirectory -import org.moire.ultrasonic.domain.PlayerState -import org.moire.ultrasonic.domain.SearchResult -import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.service.DownloadFile -import timber.log.Timber private const val LINE_LENGTH = 60 private const val DEGRADE_PRECISION_AFTER = 10 @@ -77,11 +68,6 @@ object Util { private var MEGA_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null private var KILO_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null private var BYTE_LOCALIZED_FORMAT: DecimalFormat? = null - private const val EVENT_META_CHANGED = "org.moire.ultrasonic.EVENT_META_CHANGED" - private const val EVENT_PLAYSTATE_CHANGED = "org.moire.ultrasonic.EVENT_PLAYSTATE_CHANGED" - private const val CM_AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged" - private const val CM_AVRCP_PLAYBACK_COMPLETE = "com.android.music.playbackcomplete" - private const val CM_AVRCP_METADATA_CHANGED = "com.android.music.metachanged" // Used by hexEncode() private val HEX_DIGITS = @@ -448,150 +434,6 @@ object Util { return musicDirectory } - /** - * Broadcasts the given song info as the new song being played. - */ - fun broadcastNewTrackInfo(context: Context, song: Track?) { - val intent = Intent(EVENT_META_CHANGED) - if (song != null) { - intent.putExtra("title", song.title) - intent.putExtra("artist", song.artist) - intent.putExtra("album", song.album) - val albumArtFile = FileUtil.getAlbumArtFile(song) - intent.putExtra("coverart", albumArtFile) - } else { - intent.putExtra("title", "") - intent.putExtra("artist", "") - intent.putExtra("album", "") - intent.putExtra("coverart", "") - } - context.sendBroadcast(intent) - } - - fun broadcastA2dpMetaDataChange( - context: Context, - playerPosition: Int, - currentPlaying: DownloadFile?, - listSize: Int, - id: Int - ) { - if (!Settings.shouldSendBluetoothNotifications) return - - var song: Track? = null - val avrcpIntent = Intent(CM_AVRCP_METADATA_CHANGED) - if (currentPlaying != null) song = currentPlaying.track - - fillIntent(avrcpIntent, song, playerPosition, id, listSize) - - context.sendBroadcast(avrcpIntent) - } - - @Suppress("LongParameterList") - fun broadcastA2dpPlayStatusChange( - context: Context, - state: PlayerState?, - newSong: Track?, - listSize: Int, - id: Int, - playerPosition: Int - ) { - if (!Settings.shouldSendBluetoothNotifications) return - - if (newSong != null) { - - val avrcpIntent = Intent( - if (state == PlayerState.COMPLETED) CM_AVRCP_PLAYBACK_COMPLETE - else CM_AVRCP_PLAYSTATE_CHANGED - ) - - fillIntent(avrcpIntent, newSong, playerPosition, id, listSize) - - if (state != PlayerState.COMPLETED) { - when (state) { - PlayerState.STARTED -> avrcpIntent.putExtra("playing", true) - PlayerState.STOPPED, - PlayerState.PAUSED -> avrcpIntent.putExtra("playing", false) - else -> return // No need to broadcast. - } - } - - context.sendBroadcast(avrcpIntent) - } - } - - private fun fillIntent( - intent: Intent, - song: Track?, - playerPosition: Int, - id: Int, - listSize: Int - ) { - if (song == null) { - intent.putExtra("track", "") - intent.putExtra("track_name", "") - intent.putExtra("artist", "") - intent.putExtra("artist_name", "") - intent.putExtra("album", "") - intent.putExtra("album_name", "") - intent.putExtra("album_artist", "") - intent.putExtra("album_artist_name", "") - - if (Settings.shouldSendBluetoothAlbumArt) { - intent.putExtra("coverart", null as Parcelable?) - intent.putExtra("cover", null as Parcelable?) - } - - intent.putExtra("ListSize", 0.toLong()) - intent.putExtra("id", 0.toLong()) - intent.putExtra("duration", 0.toLong()) - intent.putExtra("position", 0.toLong()) - } else { - val title = song.title - val artist = song.artist - val album = song.album - val duration = song.duration - - intent.putExtra("track", title) - intent.putExtra("track_name", title) - intent.putExtra("artist", artist) - intent.putExtra("artist_name", artist) - intent.putExtra("album", album) - intent.putExtra("album_name", album) - intent.putExtra("album_artist", artist) - intent.putExtra("album_artist_name", artist) - - if (Settings.shouldSendBluetoothAlbumArt) { - val albumArtFile = FileUtil.getAlbumArtFile(song) - intent.putExtra("coverart", albumArtFile) - intent.putExtra("cover", albumArtFile) - } - - intent.putExtra("position", playerPosition.toLong()) - intent.putExtra("id", id.toLong()) - intent.putExtra("ListSize", listSize.toLong()) - - if (duration != null) { - intent.putExtra("duration", duration.toLong()) - } - } - } - - /** - * - * Broadcasts the given player state as the one being set. - */ - fun broadcastPlaybackStatusChange(context: Context, state: PlayerState?) { - val intent = Intent(EVENT_PLAYSTATE_CHANGED) - when (state) { - PlayerState.STARTED -> intent.putExtra("state", "play") - PlayerState.STOPPED -> intent.putExtra("state", "stop") - PlayerState.PAUSED -> intent.putExtra("state", "pause") - PlayerState.COMPLETED -> intent.putExtra("state", "complete") - else -> return // No need to broadcast. - } - context.sendBroadcast(intent) - } - @JvmStatic @Suppress("MagicNumber") fun getNotificationImageSize(context: Context): Int { @@ -667,7 +509,7 @@ object Util { val hours = TimeUnit.MILLISECONDS.toHours(millis) val minutes = TimeUnit.MILLISECONDS.toMinutes(millis) - TimeUnit.HOURS.toMinutes(hours) val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) - - TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes) + TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes) return when { hours >= DEGRADE_PRECISION_AFTER -> { @@ -761,9 +603,9 @@ object Util { fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri { return Uri.parse( ContentResolver.SCHEME_ANDROID_RESOURCE + - "://" + context.resources.getResourcePackageName(drawableId) + - '/' + context.resources.getResourceTypeName(drawableId) + - '/' + context.resources.getResourceEntryName(drawableId) + "://" + context.resources.getResourcePackageName(drawableId) + + '/' + context.resources.getResourceTypeName(drawableId) + + '/' + context.resources.getResourceEntryName(drawableId) ) } @@ -776,39 +618,6 @@ object Util { var fileFormat: String?, ) - fun getMediaDescriptionForEntry( - song: Track, - mediaId: String? = null, - groupNameId: Int? = null - ): MediaDescriptionCompat { - - val descriptionBuilder = MediaDescriptionCompat.Builder() - val desc = readableEntryDescription(song) - val title: String - - if (groupNameId != null) - descriptionBuilder.setExtras( - Bundle().apply { - putString( - MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, - appContext().getString(groupNameId) - ) - } - ) - - if (desc.trackNumber.isNotEmpty()) { - title = "${desc.trackNumber} - ${desc.title}" - } else { - title = desc.title - } - - descriptionBuilder.setTitle(title) - descriptionBuilder.setSubtitle(desc.artist) - descriptionBuilder.setMediaId(mediaId) - - return descriptionBuilder.build() - } - @Suppress("ComplexMethod", "LongMethod") fun readableEntryDescription(song: Track): ReadableEntryDescription { val artist = StringBuilder(LINE_LENGTH) @@ -834,8 +643,8 @@ object Util { if (artistName != null) { if (Settings.shouldDisplayBitrateWithArtist && ( - !bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank() - ) + !bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank() + ) ) { artist.append(artistName).append(" (").append( String.format( @@ -880,18 +689,6 @@ object Util { ) } - 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) - } - fun getConnectivityManager(): ConnectivityManager { val context = appContext() return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager