diff --git a/app/src/play/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt b/app/src/play/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt deleted file mode 100644 index f88f9b6b..00000000 --- a/app/src/play/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt +++ /dev/null @@ -1,497 +0,0 @@ -package com.cappielloantonio.tempo.service - -import android.net.Uri -import androidx.lifecycle.LifecycleOwner -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaItem.SubtitleConfiguration -import androidx.media3.common.MediaMetadata -import androidx.media3.session.LibraryResult -import com.cappielloantonio.tempo.repository.AutomotiveRepository -import com.cappielloantonio.tempo.util.Preferences.getServerId -import com.google.common.collect.ImmutableList -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture -import com.google.common.util.concurrent.SettableFuture - -object MediaBrowserTree { - - private lateinit var automotiveRepository: AutomotiveRepository - - private var treeNodes: MutableMap = mutableMapOf() - - private var isInitialized = false - - // Root - private const val ROOT_ID = "[rootID]" - - // First level - private const val HOME_ID = "[homeID]" - private const val LIBRARY_ID = "[libraryID]" - private const val OTHER_ID = "[otherID]" - - // Second level HOME_ID - private const val MOST_PLAYED_ID = "[mostPlayedID]" - private const val LAST_PLAYED_ID = "[lastPlayedID]" - private const val RECENTLY_ADDED_ID = "[recentlyAddedID]" - private const val RECENT_SONGS_ID = "[recentSongsID]" - private const val MADE_FOR_YOU_ID = "[madeForYouID]" - private const val STARRED_TRACKS_ID = "[starredTracksID]" - private const val STARRED_ALBUMS_ID = "[starredAlbumsID]" - private const val STARRED_ARTISTS_ID = "[starredArtistsID]" - private const val RANDOM_ID = "[randomID]" - - // Second level LIBRARY_ID - private const val FOLDER_ID = "[folderID]" - private const val INDEX_ID = "[indexID]" - private const val DIRECTORY_ID = "[directoryID]" - private const val PLAYLIST_ID = "[playlistID]" - - // Second level OTHER_ID - private const val PODCAST_ID = "[podcastID]" - private const val RADIO_ID = "[radioID]" - - private const val ALBUM_ID = "[albumID]" - private const val ARTIST_ID = "[artistID]" - - private class MediaItemNode(val item: MediaItem) { - private val children: MutableList = ArrayList() - - fun addChild(childID: String) { - this.children.add(treeNodes[childID]!!.item) - } - - fun getChildren(): ListenableFuture>> { - val listenableFuture = SettableFuture.create>>() - val libraryResult = LibraryResult.ofItemList(children, null) - - listenableFuture.set(libraryResult) - - return listenableFuture - } - } - - private fun buildMediaItem( - title: String, - mediaId: String, - isPlayable: Boolean, - isBrowsable: Boolean, - mediaType: @MediaMetadata.MediaType Int, - subtitleConfigurations: List = mutableListOf(), - 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) - .setIsBrowsable(isBrowsable) - .setIsPlayable(isPlayable) - .setArtworkUri(imageUri) - .setMediaType(mediaType) - .build() - - return MediaItem.Builder() - .setMediaId(mediaId) - .setSubtitleConfigurations(subtitleConfigurations) - .setMediaMetadata(metadata) - .setUri(sourceUri) - .build() - } - - fun initialize(automotiveRepository: AutomotiveRepository) { - this.automotiveRepository = automotiveRepository - - if (isInitialized) return - - isInitialized = true - - // Root level - - treeNodes[ROOT_ID] = - MediaItemNode( - buildMediaItem( - title = "Root Folder", - mediaId = ROOT_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED - ) - ) - - // First level - - treeNodes[HOME_ID] = - MediaItemNode( - buildMediaItem( - title = "Home", - mediaId = HOME_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED - ) - ) - - treeNodes[LIBRARY_ID] = - MediaItemNode( - buildMediaItem( - title = "Library", - mediaId = LIBRARY_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED - ) - ) - - treeNodes[OTHER_ID] = - MediaItemNode( - buildMediaItem( - title = "Other", - mediaId = OTHER_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED - ) - ) - - treeNodes[ROOT_ID]!!.addChild(HOME_ID) - treeNodes[ROOT_ID]!!.addChild(LIBRARY_ID) - treeNodes[ROOT_ID]!!.addChild(OTHER_ID) - - // Second level HOME_ID - - treeNodes[MOST_PLAYED_ID] = - MediaItemNode( - buildMediaItem( - title = "Most played", - mediaId = MOST_PLAYED_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS - ) - ) - - treeNodes[LAST_PLAYED_ID] = - MediaItemNode( - buildMediaItem( - title = "Last played", - mediaId = LAST_PLAYED_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS - ) - ) - - treeNodes[RECENTLY_ADDED_ID] = - MediaItemNode( - buildMediaItem( - title = "Recently added", - mediaId = RECENTLY_ADDED_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS - ) - ) - - treeNodes[RECENT_SONGS_ID] = - MediaItemNode( - buildMediaItem( - title = "Recent songs", - mediaId = RECENT_SONGS_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED - ) - ) - - treeNodes[MADE_FOR_YOU_ID] = - MediaItemNode( - buildMediaItem( - title = "Made for you", - mediaId = MADE_FOR_YOU_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS - ) - ) - - treeNodes[STARRED_TRACKS_ID] = - MediaItemNode( - buildMediaItem( - title = "Starred tracks", - mediaId = STARRED_TRACKS_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED - ) - ) - - treeNodes[STARRED_ALBUMS_ID] = - MediaItemNode( - buildMediaItem( - title = "Starred albums", - mediaId = STARRED_ALBUMS_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS - ) - ) - - treeNodes[STARRED_ARTISTS_ID] = - MediaItemNode( - buildMediaItem( - title = "Starred artists", - mediaId = STARRED_ARTISTS_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS - ) - ) - - treeNodes[RANDOM_ID] = - MediaItemNode( - buildMediaItem( - title = "Random", - mediaId = RANDOM_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED - ) - ) - - treeNodes[HOME_ID]!!.addChild(MOST_PLAYED_ID) - treeNodes[HOME_ID]!!.addChild(LAST_PLAYED_ID) - treeNodes[HOME_ID]!!.addChild(RECENTLY_ADDED_ID) - treeNodes[HOME_ID]!!.addChild(RECENT_SONGS_ID) - treeNodes[HOME_ID]!!.addChild(MADE_FOR_YOU_ID) - treeNodes[HOME_ID]!!.addChild(STARRED_TRACKS_ID) - treeNodes[HOME_ID]!!.addChild(STARRED_ALBUMS_ID) - treeNodes[HOME_ID]!!.addChild(STARRED_ARTISTS_ID) - treeNodes[HOME_ID]!!.addChild(RANDOM_ID) - - // Second level LIBRARY_ID - - treeNodes[FOLDER_ID] = - MediaItemNode( - buildMediaItem( - title = "Folders", - mediaId = FOLDER_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED - ) - ) - - treeNodes[PLAYLIST_ID] = - MediaItemNode( - buildMediaItem( - title = "Playlists", - mediaId = PLAYLIST_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS - ) - ) - - treeNodes[LIBRARY_ID]!!.addChild(FOLDER_ID) - treeNodes[LIBRARY_ID]!!.addChild(PLAYLIST_ID) - - // Second level OTHER_ID - - treeNodes[PODCAST_ID] = - MediaItemNode( - buildMediaItem( - title = "Podcasts", - mediaId = PODCAST_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PODCASTS - ) - ) - - treeNodes[RADIO_ID] = - MediaItemNode( - buildMediaItem( - title = "Radio stations", - mediaId = RADIO_ID, - isPlayable = false, - isBrowsable = true, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_RADIO_STATIONS - ) - ) - - treeNodes[OTHER_ID]!!.addChild(PODCAST_ID) - treeNodes[OTHER_ID]!!.addChild(RADIO_ID) - } - - fun getRootItem(): MediaItem { - return treeNodes[ROOT_ID]!!.item - } - - fun getChildren( - id: String - ): ListenableFuture>> { - return when (id) { - ROOT_ID -> treeNodes[ROOT_ID]?.getChildren()!! - HOME_ID -> treeNodes[HOME_ID]?.getChildren()!! - LIBRARY_ID -> treeNodes[LIBRARY_ID]?.getChildren()!! - OTHER_ID -> treeNodes[OTHER_ID]?.getChildren()!! - - MOST_PLAYED_ID -> automotiveRepository.getAlbums(id, "frequent", 100) - LAST_PLAYED_ID -> automotiveRepository.getAlbums(id, "recent", 100) - RECENTLY_ADDED_ID -> automotiveRepository.getAlbums(id, "newest", 100) - RECENT_SONGS_ID -> automotiveRepository.getRecentlyPlayedSongs(getServerId(),100) - MADE_FOR_YOU_ID -> automotiveRepository.getStarredArtists(id) - STARRED_TRACKS_ID -> automotiveRepository.starredSongs - STARRED_ALBUMS_ID -> automotiveRepository.getStarredAlbums(id) - STARRED_ARTISTS_ID -> automotiveRepository.getStarredArtists(id) - RANDOM_ID -> automotiveRepository.getRandomSongs(100) - FOLDER_ID -> automotiveRepository.getMusicFolders(id) - PLAYLIST_ID -> automotiveRepository.getPlaylists(id) - PODCAST_ID -> automotiveRepository.getNewestPodcastEpisodes(100) - RADIO_ID -> automotiveRepository.internetRadioStations - - else -> { - if (id.startsWith(MOST_PLAYED_ID)) { - return automotiveRepository.getAlbumTracks( - id.removePrefix( - MOST_PLAYED_ID - ) - ) - } - - if (id.startsWith(LAST_PLAYED_ID)) { - return automotiveRepository.getAlbumTracks( - id.removePrefix( - LAST_PLAYED_ID - ) - ) - } - - if (id.startsWith(RECENTLY_ADDED_ID)) { - return automotiveRepository.getAlbumTracks( - id.removePrefix( - RECENTLY_ADDED_ID - ) - ) - } - - if (id.startsWith(MADE_FOR_YOU_ID)) { - return automotiveRepository.getMadeForYou( - id.removePrefix( - MADE_FOR_YOU_ID - ), - 20 - ) - } - - if (id.startsWith(STARRED_ALBUMS_ID)) { - return automotiveRepository.getAlbumTracks( - id.removePrefix( - STARRED_ALBUMS_ID - ) - ) - } - - if (id.startsWith(STARRED_ARTISTS_ID)) { - return automotiveRepository.getArtistAlbum( - STARRED_ALBUMS_ID, - id.removePrefix( - STARRED_ARTISTS_ID - ) - ) - } - - if (id.startsWith(FOLDER_ID)) { - return automotiveRepository.getIndexes( - INDEX_ID, - id.removePrefix( - FOLDER_ID - ) - ) - } - - if (id.startsWith(INDEX_ID)) { - return automotiveRepository.getDirectories( - DIRECTORY_ID, - id.removePrefix( - INDEX_ID - ) - ) - } - - if (id.startsWith(DIRECTORY_ID)) { - return automotiveRepository.getDirectories( - DIRECTORY_ID, - id.removePrefix( - DIRECTORY_ID - ) - ) - } - - if (id.startsWith(PLAYLIST_ID)) { - return automotiveRepository.getPlaylistSongs( - id.removePrefix( - PLAYLIST_ID - ) - ) - } - - if (id.startsWith(ALBUM_ID)) { - return automotiveRepository.getAlbumTracks( - id.removePrefix( - ALBUM_ID - ) - ) - } - - if (id.startsWith(ARTIST_ID)) { - return automotiveRepository.getArtistAlbum( - ALBUM_ID, - id.removePrefix( - ARTIST_ID - ) - ) - } - - return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)) - } - } - } - - // https://github.com/androidx/media/issues/156 - fun getItems(mediaItems: List): List { - val updatedMediaItems = ArrayList() - - mediaItems.forEach { - if (it.localConfiguration?.uri != null) { - updatedMediaItems.add(it) - } else { - val sessionMediaItem = automotiveRepository.getSessionMediaItem(it.mediaId) - - if (sessionMediaItem != null) { - var toAdd = automotiveRepository.getMetadatas(sessionMediaItem.timestamp!!) - val index = toAdd.indexOfFirst { mediaItem -> mediaItem.mediaId == it.mediaId } - - toAdd = toAdd.subList(index, toAdd.size) - - updatedMediaItems.addAll(toAdd) - } - } - } - - return updatedMediaItems - } - - fun search(query: String): ListenableFuture>> { - return automotiveRepository.search( - query, - ALBUM_ID, - ARTIST_ID - ) - } -} diff --git a/app/src/play/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt b/app/src/play/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt deleted file mode 100644 index 099ae672..00000000 --- a/app/src/play/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt +++ /dev/null @@ -1,202 +0,0 @@ -package com.cappielloantonio.tempo.service - -import android.content.Context -import android.os.Bundle -import androidx.annotation.OptIn -import androidx.media3.common.MediaItem -import androidx.media3.common.Player -import androidx.media3.common.util.UnstableApi -import androidx.media3.session.CommandButton -import androidx.media3.session.LibraryResult -import androidx.media3.session.MediaLibraryService -import androidx.media3.session.MediaSession -import androidx.media3.session.SessionCommand -import androidx.media3.session.SessionResult -import com.cappielloantonio.tempo.R -import com.cappielloantonio.tempo.repository.AutomotiveRepository -import com.google.common.collect.ImmutableList -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture - -open class MediaLibrarySessionCallback( - context: Context, - automotiveRepository: AutomotiveRepository -) : - MediaLibraryService.MediaLibrarySession.Callback { - - init { - MediaBrowserTree.initialize(automotiveRepository) - } - - private val shuffleCommandButtons: List = listOf( - CommandButton.Builder() - .setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description)) - .setSessionCommand( - SessionCommand( - CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY - ) - ).setIconResId(R.drawable.exo_icon_shuffle_off).build(), - - CommandButton.Builder() - .setDisplayName(context.getString(R.string.exo_controls_shuffle_off_description)) - .setSessionCommand( - SessionCommand( - CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY - ) - ).setIconResId(R.drawable.exo_icon_shuffle_on).build() - ) - - private val repeatCommandButtons: List = listOf( - CommandButton.Builder() - .setDisplayName(context.getString(R.string.exo_controls_repeat_off_description)) - .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, Bundle.EMPTY)) - .setIconResId(R.drawable.exo_icon_repeat_off) - .build(), - CommandButton.Builder() - .setDisplayName(context.getString(R.string.exo_controls_repeat_one_description)) - .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE, Bundle.EMPTY)) - .setIconResId(R.drawable.exo_icon_repeat_one) - .build(), - CommandButton.Builder() - .setDisplayName(context.getString(R.string.exo_controls_repeat_all_description)) - .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, Bundle.EMPTY)) - .setIconResId(R.drawable.exo_icon_repeat_all) - .build() - ) - - private val customLayoutCommandButtons: List = - shuffleCommandButtons + repeatCommandButtons - - @OptIn(UnstableApi::class) - val mediaNotificationSessionCommands = - MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon() - .also { builder -> - (shuffleCommandButtons + repeatCommandButtons).forEach { commandButton -> - commandButton.sessionCommand?.let { builder.add(it) } - } - }.build() - - fun buildCustomLayout(player: Player): ImmutableList { - val shuffle = shuffleCommandButtons[if (player.shuffleModeEnabled) 1 else 0] - val repeat = when (player.repeatMode) { - Player.REPEAT_MODE_ONE -> repeatCommandButtons[1] - Player.REPEAT_MODE_ALL -> repeatCommandButtons[2] - else -> repeatCommandButtons[0] - } - return ImmutableList.of(shuffle, repeat) - } - - @OptIn(UnstableApi::class) - override fun onConnect( - session: MediaSession, controller: MediaSession.ControllerInfo - ): MediaSession.ConnectionResult { - if (session.isMediaNotificationController(controller) || session.isAutomotiveController( - controller - ) || session.isAutoCompanionController(controller) - ) { - val customLayout = buildCustomLayout(session.player) - - return MediaSession.ConnectionResult.AcceptedResultBuilder(session) - .setAvailableSessionCommands(mediaNotificationSessionCommands) - .setCustomLayout(customLayout).build() - } - - return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build() - } - - @OptIn(UnstableApi::class) - override fun onCustomCommand( - session: MediaSession, - controller: MediaSession.ControllerInfo, - customCommand: SessionCommand, - args: Bundle - ): ListenableFuture { - when (customCommand.customAction) { - CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true - CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false - CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, - CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, - CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> { - val nextMode = when (session.player.repeatMode) { - Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL - Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ONE - else -> Player.REPEAT_MODE_OFF - } - session.player.repeatMode = nextMode - } - else -> return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) - } - - session.setCustomLayout( - session.mediaNotificationControllerInfo!!, - buildCustomLayout(session.player) - ) - - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } - - override fun onGetLibraryRoot( - session: MediaLibraryService.MediaLibrarySession, - browser: MediaSession.ControllerInfo, - params: MediaLibraryService.LibraryParams? - ): ListenableFuture> { - return Futures.immediateFuture(LibraryResult.ofItem(MediaBrowserTree.getRootItem(), params)) - } - - override fun onGetChildren( - session: MediaLibraryService.MediaLibrarySession, - browser: MediaSession.ControllerInfo, - parentId: String, - page: Int, - pageSize: Int, - params: MediaLibraryService.LibraryParams? - ): ListenableFuture>> { - return MediaBrowserTree.getChildren(parentId) - } - - override fun onAddMediaItems( - mediaSession: MediaSession, - controller: MediaSession.ControllerInfo, - mediaItems: List - ): ListenableFuture> { - return super.onAddMediaItems( - mediaSession, - controller, - MediaBrowserTree.getItems(mediaItems) - ) - } - - override fun onSearch( - session: MediaLibraryService.MediaLibrarySession, - browser: MediaSession.ControllerInfo, - query: String, - params: MediaLibraryService.LibraryParams? - ): ListenableFuture> { - session.notifySearchResultChanged(browser, query, 60, params) - return Futures.immediateFuture(LibraryResult.ofVoid()) - } - - override fun onGetSearchResult( - session: MediaLibraryService.MediaLibrarySession, - browser: MediaSession.ControllerInfo, - query: String, - page: Int, - pageSize: Int, - params: MediaLibraryService.LibraryParams? - ): ListenableFuture>> { - return MediaBrowserTree.search(query) - } - - companion object { - private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON = - "android.media3.session.demo.SHUFFLE_ON" - private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = - "android.media3.session.demo.SHUFFLE_OFF" - private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = - "android.media3.session.demo.REPEAT_OFF" - private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = - "android.media3.session.demo.REPEAT_ONE" - private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = - "android.media3.session.demo.REPEAT_ALL" - } -} \ No newline at end of file diff --git a/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt deleted file mode 100644 index a73a7c33..00000000 --- a/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt +++ /dev/null @@ -1,363 +0,0 @@ -package com.cappielloantonio.tempo.service - -import android.app.PendingIntent.FLAG_IMMUTABLE -import android.app.PendingIntent.FLAG_UPDATE_CURRENT -import android.app.TaskStackBuilder -import android.content.Intent -import android.os.Binder -import android.os.IBinder -import android.os.Handler -import android.os.Looper -import androidx.media3.cast.CastPlayer -import androidx.media3.cast.SessionAvailabilityListener -import androidx.media3.common.AudioAttributes -import androidx.media3.common.C -import androidx.media3.common.MediaItem -import androidx.media3.common.Player -import androidx.media3.common.Tracks -import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.DefaultLoadControl -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.session.MediaLibraryService -import androidx.media3.session.MediaSession.ControllerInfo -import com.cappielloantonio.tempo.repository.AutomotiveRepository -import com.cappielloantonio.tempo.ui.activity.MainActivity -import com.cappielloantonio.tempo.util.Constants -import com.cappielloantonio.tempo.util.DownloadUtil -import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory -import com.cappielloantonio.tempo.util.Preferences -import com.cappielloantonio.tempo.util.ReplayGainUtil -import com.cappielloantonio.tempo.widget.WidgetUpdateManager -import com.google.android.gms.cast.framework.CastContext -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability - -@UnstableApi -class MediaService : MediaLibraryService(), SessionAvailabilityListener { - private lateinit var automotiveRepository: AutomotiveRepository - private lateinit var player: ExoPlayer - private lateinit var castPlayer: CastPlayer - private lateinit var mediaLibrarySession: MediaLibrarySession - private lateinit var librarySessionCallback: MediaLibrarySessionCallback - lateinit var equalizerManager: EqualizerManager - - inner class LocalBinder : Binder() { - fun getEqualizerManager(): EqualizerManager { - return this@MediaService.equalizerManager - } - } - - private val binder = LocalBinder() - - companion object { - const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER" - } - private val widgetUpdateHandler = Handler(Looper.getMainLooper()) - private var widgetUpdateScheduled = false - private val widgetUpdateRunnable = object : Runnable { - override fun run() { - if (!player.isPlaying) { - widgetUpdateScheduled = false - return - } - updateWidget() - widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS) - } - } - - override fun onCreate() { - super.onCreate() - - initializeRepository() - initializePlayer() - initializeCastPlayer() - initializeMediaLibrarySession() - initializePlayerListener() - initializeEqualizerManager() - - setPlayer( - null, - if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable) castPlayer else player - ) - } - - override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession { - return mediaLibrarySession - } - - override fun onTaskRemoved(rootIntent: Intent?) { - val player = mediaLibrarySession.player - - if (!player.playWhenReady || player.mediaItemCount == 0) { - stopSelf() - } - } - - override fun onDestroy() { - equalizerManager.release() - stopWidgetUpdates() - releasePlayer() - super.onDestroy() - } - - override fun onBind(intent: Intent?): IBinder? { - // Check if the intent is for our custom equalizer binder - if (intent?.action == ACTION_BIND_EQUALIZER) { - return binder - } - // Otherwise, handle it as a normal MediaLibraryService connection - return super.onBind(intent) - } - - private fun initializeRepository() { - automotiveRepository = AutomotiveRepository() - } - - private fun initializePlayer() { - player = ExoPlayer.Builder(this) - .setRenderersFactory(getRenderersFactory()) - .setMediaSourceFactory(DynamicMediaSourceFactory(this)) - .setAudioAttributes(AudioAttributes.DEFAULT, true) - .setHandleAudioBecomingNoisy(true) - .setWakeMode(C.WAKE_MODE_NETWORK) - .setLoadControl(initializeLoadControl()) - .build() - - player.shuffleModeEnabled = Preferences.isShuffleModeEnabled() - player.repeatMode = Preferences.getRepeatMode() - } - - private fun initializeEqualizerManager() { - equalizerManager = EqualizerManager() - val audioSessionId = player.audioSessionId - if (equalizerManager.attachToSession(audioSessionId)) { - val enabled = Preferences.isEqualizerEnabled() - equalizerManager.setEnabled(enabled) - - val bands = equalizerManager.getNumberOfBands() - val savedLevels = Preferences.getEqualizerBandLevels(bands) - for (i in 0 until bands) { - equalizerManager.setBandLevel(i.toShort(), savedLevels[i]) - } - } - } - - private fun initializeCastPlayer() { - if (GoogleApiAvailability.getInstance() - .isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS - ) { - castPlayer = CastPlayer(CastContext.getSharedInstance(this)) - castPlayer.setSessionAvailabilityListener(this) - } - } - - private fun initializeMediaLibrarySession() { - val sessionActivityPendingIntent = - TaskStackBuilder.create(this).run { - addNextIntent(Intent(this@MediaService, MainActivity::class.java)) - getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT) - } - - librarySessionCallback = createLibrarySessionCallback() - mediaLibrarySession = - MediaLibrarySession.Builder(this, player, librarySessionCallback) - .setSessionActivity(sessionActivityPendingIntent) - .build() - } - - private fun createLibrarySessionCallback(): MediaLibrarySessionCallback { - return MediaLibrarySessionCallback(this, automotiveRepository) - } - - private fun initializePlayerListener() { - player.addListener(object : Player.Listener { - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - if (mediaItem == null) return - - if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { - MediaManager.setLastPlayedTimestamp(mediaItem) - } - updateWidget() - } - - override fun onTracksChanged(tracks: Tracks) { - ReplayGainUtil.setReplayGain(player, tracks) - val currentMediaItem = player.currentMediaItem - if (currentMediaItem != null && currentMediaItem.mediaMetadata.extras != null) { - MediaManager.scrobble(currentMediaItem, false) - } - - if (player.currentMediaItemIndex + 1 == player.mediaItemCount) - MediaManager.continuousPlay(player.currentMediaItem) - } - - override fun onIsPlayingChanged(isPlaying: Boolean) { - if (!isPlaying) { - MediaManager.setPlayingPausedTimestamp( - player.currentMediaItem, - player.currentPosition - ) - } else { - MediaManager.scrobble(player.currentMediaItem, false) - } - if (isPlaying) { - scheduleWidgetUpdates() - } else { - stopWidgetUpdates() - } - updateWidget() - } - - override fun onPlaybackStateChanged(playbackState: Int) { - super.onPlaybackStateChanged(playbackState) - - if (!player.hasNextMediaItem() && - playbackState == Player.STATE_ENDED && - player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC - ) { - MediaManager.scrobble(player.currentMediaItem, true) - MediaManager.saveChronology(player.currentMediaItem) - } - updateWidget() - } - - override fun onPositionDiscontinuity( - oldPosition: Player.PositionInfo, - newPosition: Player.PositionInfo, - reason: Int - ) { - super.onPositionDiscontinuity(oldPosition, newPosition, reason) - - if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { - if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) { - MediaManager.scrobble(oldPosition.mediaItem, true) - MediaManager.saveChronology(oldPosition.mediaItem) - } - - if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) { - MediaManager.setLastPlayedTimestamp(newPosition.mediaItem) - } - } - } - - override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { - Preferences.setShuffleModeEnabled(shuffleModeEnabled) - mediaLibrarySession.setCustomLayout( - librarySessionCallback.buildCustomLayout(player) - ) - } - - override fun onRepeatModeChanged(repeatMode: Int) { - Preferences.setRepeatMode(repeatMode) - mediaLibrarySession.setCustomLayout( - librarySessionCallback.buildCustomLayout(player) - ) - } - }) - if (player.isPlaying) { - scheduleWidgetUpdates() - } - } - - private fun updateWidget() { - val mi = player.currentMediaItem - val title = mi?.mediaMetadata?.title?.toString() - ?: mi?.mediaMetadata?.extras?.getString("title") - val artist = mi?.mediaMetadata?.artist?.toString() - ?: mi?.mediaMetadata?.extras?.getString("artist") - val album = mi?.mediaMetadata?.albumTitle?.toString() - ?: mi?.mediaMetadata?.extras?.getString("album") - val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId") - - val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L - val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L - WidgetUpdateManager.updateFromState( - this, - title ?: "", - artist ?: "", - album ?: "", - coverId, - player.isPlaying, - player.shuffleModeEnabled, - player.repeatMode, - position, - duration - ) - } - - private fun scheduleWidgetUpdates() { - if (widgetUpdateScheduled) return - widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS) - widgetUpdateScheduled = true - } - - private fun stopWidgetUpdates() { - if (!widgetUpdateScheduled) return - widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable) - widgetUpdateScheduled = false - } - - private fun initializeLoadControl(): DefaultLoadControl { - return DefaultLoadControl.Builder() - .setBufferDurationsMs( - (DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(), - (DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(), - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS - ) - .build() - } - - private fun getQueueFromPlayer(player: Player): List { - val queue = mutableListOf() - for (i in 0 until player.mediaItemCount) { - queue.add(player.getMediaItemAt(i)) - } - return queue - } - - private fun setPlayer(oldPlayer: Player?, newPlayer: Player) { - if (oldPlayer === newPlayer) return - - oldPlayer?.stop() - mediaLibrarySession.player = newPlayer - } - - private fun releasePlayer() { - if (this::castPlayer.isInitialized) castPlayer.setSessionAvailabilityListener(null) - if (this::castPlayer.isInitialized) castPlayer.release() - player.release() - mediaLibrarySession.release() - automotiveRepository.deleteMetadata() - } - - private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) - - override fun onCastSessionAvailable() { - val currentQueue = getQueueFromPlayer(player) - val currentIndex = player.currentMediaItemIndex - val currentPosition = player.currentPosition - val isPlaying = player.playWhenReady - - setPlayer(player, castPlayer) - - castPlayer.setMediaItems(currentQueue, currentIndex, currentPosition) - castPlayer.playWhenReady = isPlaying - castPlayer.prepare() - } - - override fun onCastSessionUnavailable() { - val currentQueue = getQueueFromPlayer(castPlayer) - val currentIndex = castPlayer.currentMediaItemIndex - val currentPosition = castPlayer.currentPosition - val isPlaying = castPlayer.playWhenReady - - setPlayer(castPlayer, player) - - player.setMediaItems(currentQueue, currentIndex, currentPosition) - player.playWhenReady = isPlaying - player.prepare() - } -} - -private const val WIDGET_UPDATE_INTERVAL_MS = 1000L diff --git a/app/src/play/java/com/cappielloantonio/tempo/ui/fragment/ToolbarFragment.java b/app/src/play/java/com/cappielloantonio/tempo/ui/fragment/ToolbarFragment.java deleted file mode 100644 index d3f36bb7..00000000 --- a/app/src/play/java/com/cappielloantonio/tempo/ui/fragment/ToolbarFragment.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.cappielloantonio.tempo.ui.fragment; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.media3.common.util.UnstableApi; - -import com.cappielloantonio.tempo.R; -import com.cappielloantonio.tempo.databinding.FragmentToolbarBinding; -import com.cappielloantonio.tempo.ui.activity.MainActivity; -import com.google.android.gms.cast.framework.CastButtonFactory; - -@UnstableApi -public class ToolbarFragment extends Fragment { - private static final String TAG = "ToolbarFragment"; - - private FragmentToolbarBinding bind; - private MainActivity activity; - - public ToolbarFragment() { - // Required empty public constructor - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - } - - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.main_page_menu, menu); - CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - activity = (MainActivity) getActivity(); - - bind = FragmentToolbarBinding.inflate(inflater, container, false); - View view = bind.getRoot(); - - return view; - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == R.id.action_search) { - activity.navController.navigate(R.id.searchFragment); - return true; - } else if (item.getItemId() == R.id.action_settings) { - activity.navController.navigate(R.id.settingsFragment); - return true; - } - - return false; - } -} \ No newline at end of file diff --git a/app/src/play/java/com/cappielloantonio/tempo/util/Flavors.java b/app/src/play/java/com/cappielloantonio/tempo/util/Flavors.java deleted file mode 100644 index 4bed2921..00000000 --- a/app/src/play/java/com/cappielloantonio/tempo/util/Flavors.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.cappielloantonio.tempo.util; - -import android.content.Context; - -import com.google.android.gms.cast.framework.CastContext; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GoogleApiAvailability; - -public class Flavors { - public static void initializeCastContext(Context context) { - if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS) - CastContext.getSharedInstance(context); - } -}