repo: add play variant

This commit is contained in:
CappielloAntonio
2024-08-22 16:11:56 +02:00
parent e465892013
commit 349c961f1a
6 changed files with 956 additions and 0 deletions

View File

@@ -37,6 +37,11 @@ android {
dimension = "default"
applicationId "com.cappielloantonio.notquitemy.tempo"
}
play {
dimension = "default"
applicationId "com.cappielloantonio.play.tempo"
}
}
buildTypes {
@@ -90,6 +95,7 @@ dependencies {
implementation 'androidx.media3:media3-ui:1.4.0'
implementation 'androidx.media3:media3-exoplayer-hls:1.4.0'
tempoImplementation 'androidx.media3:media3-cast:1.4.0'
playImplementation 'androidx.media3:media3-cast:1.4.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
annotationProcessor 'androidx.room:room-compiler:2.6.1'

View File

@@ -0,0 +1,497 @@
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<String, MediaItemNode> = 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<MediaItem> = ArrayList()
fun addChild(childID: String) {
this.children.add(treeNodes[childID]!!.item)
}
fun getChildren(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val listenableFuture = SettableFuture.create<LibraryResult<ImmutableList<MediaItem>>>()
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<SubtitleConfiguration> = 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<LibraryResult<ImmutableList<MediaItem>>> {
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<MediaItem>): List<MediaItem> {
val updatedMediaItems = ArrayList<MediaItem>()
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<LibraryResult<ImmutableList<MediaItem>>> {
return automotiveRepository.search(
query,
ALBUM_ID,
ARTIST_ID
)
}
}

View File

@@ -0,0 +1,162 @@
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.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 customLayoutCommandButtons: List<CommandButton> = 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()
)
@OptIn(UnstableApi::class)
val mediaNotificationSessionCommands =
MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
.also { builder ->
customLayoutCommandButtons.forEach { commandButton ->
commandButton.sessionCommand?.let { builder.add(it) }
}
}.build()
@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 =
customLayoutCommandButtons[if (session.player.shuffleModeEnabled) 1 else 0]
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(mediaNotificationSessionCommands)
.setCustomLayout(ImmutableList.of(customLayout)).build()
}
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
}
@OptIn(UnstableApi::class)
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
session.player.shuffleModeEnabled = true
session.setCustomLayout(
session.mediaNotificationControllerInfo!!,
ImmutableList.of(customLayoutCommandButtons[1])
)
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
session.player.shuffleModeEnabled = false
session.setCustomLayout(
session.mediaNotificationControllerInfo!!,
ImmutableList.of(customLayoutCommandButtons[0])
)
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED))
}
override fun onGetLibraryRoot(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> {
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<LibraryResult<ImmutableList<MediaItem>>> {
return MediaBrowserTree.getChildren(parentId)
}
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
return super.onAddMediaItems(
mediaSession,
controller,
MediaBrowserTree.getItems(mediaItems)
)
}
override fun onSearch(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
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<LibraryResult<ImmutableList<MediaItem>>> {
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"
}
}

View File

@@ -0,0 +1,210 @@
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 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.exoplayer.source.DefaultMediaSourceFactory
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.Preferences
import com.cappielloantonio.tempo.util.ReplayGainUtil
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
override fun onCreate() {
super.onCreate()
initializeRepository()
initializePlayer()
initializeCastPlayer()
initializeMediaLibrarySession()
initializePlayerListener()
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() {
releasePlayer()
super.onDestroy()
}
private fun initializeRepository() {
automotiveRepository = AutomotiveRepository()
}
private fun initializePlayer() {
player = ExoPlayer.Builder(this)
.setRenderersFactory(getRenderersFactory())
.setMediaSourceFactory(getMediaSourceFactory())
.setAudioAttributes(AudioAttributes.DEFAULT, true)
.setHandleAudioBecomingNoisy(true)
.setWakeMode(C.WAKE_MODE_NETWORK)
.setLoadControl(initializeLoadControl())
.build()
}
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)
}
mediaLibrarySession =
MediaLibrarySession.Builder(this, player, createLibrarySessionCallback())
.setSessionActivity(sessionActivityPendingIntent)
.build()
}
private fun createLibrarySessionCallback(): MediaLibrarySession.Callback {
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)
}
}
override fun onTracksChanged(tracks: Tracks) {
ReplayGainUtil.setReplayGain(player, tracks)
MediaManager.scrobble(player.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)
}
}
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)
}
}
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)
}
}
}
})
}
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 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()
clearListener()
}
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
private fun getMediaSourceFactory() =
DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this))
override fun onCastSessionAvailable() {
setPlayer(player, castPlayer)
}
override fun onCastSessionUnavailable() {
setPlayer(castPlayer, player)
}
}

View File

@@ -0,0 +1,67 @@
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;
}
}

View File

@@ -0,0 +1,14 @@
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);
}
}