From d6cc4fc028fa9d52d04963d3e0128f8f6de9a30a Mon Sep 17 00:00:00 2001 From: antonio Date: Wed, 3 Jan 2024 00:45:22 +0100 Subject: [PATCH] feat: test: Implemented initial functional version with Android Auto support --- .../tempo/database/AppDatabase.java | 10 +- .../database/dao/SessionMediaItemDao.java | 29 ++ .../tempo/model/SessionMediaItem.kt | 279 ++++++++++++ .../repository/AutomotiveRepository.java | 424 ++++++++++++++++-- .../tempo/util/MappingUtil.java | 14 +- .../tempo/service/MediaBrowserTree.kt | 75 +++- .../service/MediaLibraryServiceCallback.kt | 86 +--- .../tempo/service/MediaService.kt | 1 + 8 files changed, 758 insertions(+), 160 deletions(-) create mode 100644 app/src/main/java/com/cappielloantonio/tempo/database/dao/SessionMediaItemDao.java create mode 100644 app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java b/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java index 7e516853..f656b3c4 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java +++ b/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java @@ -14,17 +14,19 @@ import com.cappielloantonio.tempo.database.dao.FavoriteDao; import com.cappielloantonio.tempo.database.dao.QueueDao; import com.cappielloantonio.tempo.database.dao.RecentSearchDao; import com.cappielloantonio.tempo.database.dao.ServerDao; +import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao; import com.cappielloantonio.tempo.model.Chronology; import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.model.Favorite; import com.cappielloantonio.tempo.model.Queue; import com.cappielloantonio.tempo.model.RecentSearch; import com.cappielloantonio.tempo.model.Server; +import com.cappielloantonio.tempo.model.SessionMediaItem; @Database( - version = 3, - entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class}, - autoMigrations = {@AutoMigration(from = 2, to = 3)} + version = 8, + entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class}, + autoMigrations = {@AutoMigration(from = 7, to = 8)} ) @TypeConverters({DateConverters.class}) public abstract class AppDatabase extends RoomDatabase { @@ -52,4 +54,6 @@ public abstract class AppDatabase extends RoomDatabase { public abstract ChronologyDao chronologyDao(); public abstract FavoriteDao favoriteDao(); + + public abstract SessionMediaItemDao sessionMediaItemDao(); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/SessionMediaItemDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/SessionMediaItemDao.java new file mode 100644 index 00000000..a3f415ad --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/SessionMediaItemDao.java @@ -0,0 +1,29 @@ +package com.cappielloantonio.tempo.database.dao; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.cappielloantonio.tempo.model.Queue; +import com.cappielloantonio.tempo.model.SessionMediaItem; + +import java.util.List; + +@Dao +public interface SessionMediaItemDao { + @Query("SELECT * FROM session_media_item WHERE id = :id") + SessionMediaItem get(String id); + + @Query("SELECT * FROM session_media_item WHERE timestamp = :timestamp") + List get(long timestamp); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + void insert(SessionMediaItem sessionMediaItem); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + void insertAll(List sessionMediaItems); + + @Query("DELETE FROM session_media_item") + void deleteAll(); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt new file mode 100644 index 00000000..8cc456ff --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt @@ -0,0 +1,279 @@ +package com.cappielloantonio.tempo.model + +import android.net.Uri +import android.os.Bundle +import androidx.annotation.Keep +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaItem.RequestMetadata +import androidx.media3.common.MediaMetadata +import androidx.media3.common.MimeTypes +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.cappielloantonio.tempo.glide.CustomGlideRequest +import com.cappielloantonio.tempo.subsonic.models.Child +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode +import com.cappielloantonio.tempo.util.Constants +import com.cappielloantonio.tempo.util.MusicUtil +import com.cappielloantonio.tempo.util.Preferences.getImageSize +import java.util.Date + +@Keep +@Entity(tableName = "session_media_item") +class SessionMediaItem() { + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "index") + var index: Int = 0 + + @ColumnInfo(name = "id") + var id: String? = null + + @ColumnInfo(name = "parent_id") + var parentId: String? = null + + @ColumnInfo(name = "is_dir") + var isDir: Boolean = false + + @ColumnInfo + var title: String? = null + + @ColumnInfo + var album: String? = null + + @ColumnInfo + var artist: String? = null + + @ColumnInfo + var track: Int? = null + + @ColumnInfo + var year: Int? = null + + @ColumnInfo + var genre: String? = null + + @ColumnInfo(name = "cover_art_id") + var coverArtId: String? = null + + @ColumnInfo + var size: Long? = null + + @ColumnInfo(name = "content_type") + var contentType: String? = null + + @ColumnInfo + var suffix: String? = null + + @ColumnInfo("transcoding_content_type") + var transcodedContentType: String? = null + + @ColumnInfo(name = "transcoded_suffix") + var transcodedSuffix: String? = null + + @ColumnInfo + var duration: Int? = null + + @ColumnInfo("bitrate") + var bitrate: Int? = null + + @ColumnInfo + var path: String? = null + + @ColumnInfo(name = "is_video") + var isVideo: Boolean = false + + @ColumnInfo(name = "user_rating") + var userRating: Int? = null + + @ColumnInfo(name = "average_rating") + var averageRating: Double? = null + + @ColumnInfo(name = "play_count") + var playCount: Long? = null + + @ColumnInfo(name = "disc_number") + var discNumber: Int? = null + + @ColumnInfo + var created: Date? = null + + @ColumnInfo + var starred: Date? = null + + @ColumnInfo(name = "album_id") + var albumId: String? = null + + @ColumnInfo(name = "artist_id") + var artistId: String? = null + + @ColumnInfo + var type: String? = null + + @ColumnInfo(name = "bookmark_position") + var bookmarkPosition: Long? = null + + @ColumnInfo(name = "original_width") + var originalWidth: Int? = null + + @ColumnInfo(name = "original_height") + var originalHeight: Int? = null + + @ColumnInfo(name = "stream_id") + var streamId: String? = null + + @ColumnInfo(name = "stream_url") + var streamUrl: String? = null + + @ColumnInfo(name = "timestamp") + var timestamp: Long? = null + + constructor(child: Child) : this() { + id = child.id + parentId = child.parentId + isDir = child.isDir + title = child.title + album = child.album + artist = child.artist + track = child.track + year = child.year + genre = child.genre + coverArtId = child.coverArtId + size = child.size + contentType = child.contentType + suffix = child.suffix + transcodedContentType = child.transcodedContentType + transcodedSuffix = child.transcodedSuffix + duration = child.duration + bitrate = child.bitrate + path = child.path + isVideo = child.isVideo + userRating = child.userRating + averageRating = child.averageRating + playCount = child.playCount + discNumber = child.discNumber + created = child.created + starred = child.starred + albumId = child.albumId + artistId = child.artistId + type = Constants.MEDIA_TYPE_MUSIC + bookmarkPosition = child.bookmarkPosition + originalWidth = child.originalWidth + originalHeight = child.originalHeight + } + + constructor(podcastEpisode: PodcastEpisode) : this() { + id = podcastEpisode.id + parentId = podcastEpisode.parentId + isDir = podcastEpisode.isDir + title = podcastEpisode.title + album = podcastEpisode.album + artist = podcastEpisode.artist + year = podcastEpisode.year + genre = podcastEpisode.genre + coverArtId = podcastEpisode.coverArtId + size = podcastEpisode.size + contentType = podcastEpisode.contentType + suffix = podcastEpisode.suffix + duration = podcastEpisode.duration + bitrate = podcastEpisode.bitrate + path = podcastEpisode.path + isVideo = podcastEpisode.isVideo + created = podcastEpisode.created + artistId = podcastEpisode.artistId + streamId = podcastEpisode.streamId + type = Constants.MEDIA_TYPE_PODCAST + } + + constructor(internetRadioStation: InternetRadioStation) : this() { + id = internetRadioStation.id + title = internetRadioStation.name + streamUrl = internetRadioStation.streamUrl + type = Constants.MEDIA_TYPE_RADIO + } + + fun getMediaItem(): MediaItem { + val uri: Uri = getStreamUri() + val artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, getImageSize())) + + val bundle = Bundle() + bundle.putString("id", id) + bundle.putString("parentId", parentId) + bundle.putBoolean("isDir", isDir) + bundle.putString("title", title) + bundle.putString("album", album) + bundle.putString("artist", artist) + bundle.putInt("track", track ?: 0) + bundle.putInt("year", year ?: 0) + bundle.putString("genre", genre) + bundle.putString("coverArtId", coverArtId) + bundle.putLong("size", size ?: 0) + bundle.putString("contentType", contentType) + bundle.putString("suffix", suffix) + bundle.putString("transcodedContentType", transcodedContentType) + bundle.putString("transcodedSuffix", transcodedSuffix) + bundle.putInt("duration", duration ?: 0) + bundle.putInt("bitrate", bitrate ?: 0) + bundle.putString("path", path) + bundle.putBoolean("isVideo", isVideo) + bundle.putInt("userRating", userRating ?: 0) + bundle.putDouble("averageRating", averageRating ?: .0) + bundle.putLong("playCount", playCount ?: 0) + bundle.putInt("discNumber", discNumber ?: 0) + bundle.putLong("created", created?.time ?: 0) + bundle.putLong("starred", starred?.time ?: 0) + bundle.putString("albumId", albumId) + bundle.putString("artistId", artistId) + bundle.putString("type", Constants.MEDIA_TYPE_MUSIC) + bundle.putLong("bookmarkPosition", bookmarkPosition ?: 0) + bundle.putInt("originalWidth", originalWidth ?: 0) + bundle.putInt("originalHeight", originalHeight ?: 0) + bundle.putString("uri", uri.toString()) + + return MediaItem.Builder() + .setMediaId(id!!) + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(MusicUtil.getReadableString(title)) + .setTrackNumber(track ?: 0) + .setDiscNumber(discNumber ?: 0) + .setReleaseYear(year ?: 0) + .setAlbumTitle(MusicUtil.getReadableString(album)) + .setArtist(MusicUtil.getReadableString(artist)) + .setArtworkUri(artworkUri) + .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) + .build() + ) + .setRequestMetadata( + RequestMetadata.Builder() + .setMediaUri(uri) + .setExtras(bundle) + .build() + ) + .setMimeType(MimeTypes.BASE_TYPE_AUDIO) + .setUri(uri) + .build() + } + + private fun getStreamUri(): Uri { + return when (type) { + Constants.MEDIA_TYPE_MUSIC -> { + MusicUtil.getStreamUri(id) + } + + Constants.MEDIA_TYPE_PODCAST -> { + MusicUtil.getStreamUri(streamId) + } + + Constants.MEDIA_TYPE_RADIO -> { + Uri.parse(streamUrl) + } + + else -> { + MusicUtil.getStreamUri(id) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java index 5420df32..2421ebec 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java @@ -9,15 +9,22 @@ import androidx.media3.common.MediaMetadata; import androidx.media3.session.LibraryResult; import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.database.AppDatabase; +import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao; import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.model.SessionMediaItem; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.Artist; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Directory; +import com.cappielloantonio.tempo.subsonic.models.Index; import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; import com.cappielloantonio.tempo.subsonic.models.MusicFolder; import com.cappielloantonio.tempo.subsonic.models.Playlist; import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; +import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.Preferences; import com.google.common.collect.ImmutableList; @@ -25,13 +32,17 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class AutomotiveRepository { + private final SessionMediaItemDao sessionMediaItemDao = AppDatabase.getInstance().sessionMediaItemDao(); + public ListenableFuture>> getAlbums(String prefix, String type, int size) { final SettableFuture>> listenableFuture = SettableFuture.create(); @@ -86,7 +97,7 @@ public class AutomotiveRepository { return listenableFuture; } - public ListenableFuture>> getStarredSongs(String prefix) { + public ListenableFuture>> getStarredSongs() { final SettableFuture>> listenableFuture = SettableFuture.create(); App.getSubsonicClientInstance(false) @@ -98,29 +109,9 @@ public class AutomotiveRepository { if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null && response.body().getSubsonicResponse().getStarred2().getSongs() != null) { List songs = response.body().getSubsonicResponse().getStarred2().getSongs(); - List mediaItems = new ArrayList<>(); + setChildrenMetadata(songs); - for (Child song : songs) { - Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(song.getCoverArtId(), Preferences.getImageSize())); - - MediaMetadata mediaMetadata = new MediaMetadata.Builder() - .setTitle(song.getTitle()) - .setAlbumTitle(song.getAlbum()) - .setArtist(song.getArtist()) - .setIsBrowsable(false) - .setIsPlayable(true) - .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC) - .setArtworkUri(artworkUri) - .build(); - - MediaItem mediaItem = new MediaItem.Builder() - .setMediaId(prefix + song.getId()) - .setMediaMetadata(mediaMetadata) - .setUri(MusicUtil.getStreamUri(song.getId())) - .build(); - - mediaItems.add(mediaItem); - } + List mediaItems = MappingUtil.mapMediaItems(songs); LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); @@ -252,7 +243,7 @@ public class AutomotiveRepository { .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getMusicFolders() != null) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getMusicFolders() != null && response.body().getSubsonicResponse().getMusicFolders().getMusicFolders() != null) { List musicFolders = response.body().getSubsonicResponse().getMusicFolders().getMusicFolders(); List mediaItems = new ArrayList<>(); @@ -291,6 +282,137 @@ public class AutomotiveRepository { return listenableFuture; } + public ListenableFuture>> getIndexes(String prefix, String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getIndexes(id, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getIndexes() != null) { + List mediaItems = new ArrayList<>(); + + if (response.body().getSubsonicResponse().getIndexes().getIndices() != null) { + List indices = response.body().getSubsonicResponse().getIndexes().getIndices(); + + for (Index index : indices) { + if (index.getArtists() != null) { + for (Artist artist : index.getArtists()) { + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(artist.getName()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ARTIST) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + artist.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + } + } + } + + if (response.body().getSubsonicResponse().getIndexes().getChildren() != null) { + List children = response.body().getSubsonicResponse().getIndexes().getChildren(); + + for (Child song : children) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(song.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(song.getTitle()) + .setAlbumTitle(song.getAlbum()) + .setArtist(song.getArtist()) + .setIsBrowsable(false) + .setIsPlayable(true) + .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + song.getId()) + .setMediaMetadata(mediaMetadata) + .setUri(MusicUtil.getStreamUri(song.getId())) + .build(); + + mediaItems.add(mediaItem); + } + + setChildrenMetadata(children); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getDirectories(String prefix, String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getMusicDirectory(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getDirectory() != null && response.body().getSubsonicResponse().getDirectory().getChildren() != null) { + Directory directory = response.body().getSubsonicResponse().getDirectory(); + + List mediaItems = new ArrayList<>(); + + for (Child child : directory.getChildren()) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(child.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(child.getTitle()) + .setIsBrowsable(child.isDir()) + .setIsPlayable(!child.isDir()) + .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(child.isDir() ? prefix + child.getId() : child.getId()) + .setMediaMetadata(mediaMetadata) + .setUri(!child.isDir() ? MusicUtil.getStreamUri(child.getId()) : Uri.parse("")) + .build(); + + mediaItems.add(mediaItem); + } + + setChildrenMetadata(directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList())); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + public ListenableFuture>> getPlaylists(String prefix) { final SettableFuture>> listenableFuture = SettableFuture.create(); @@ -373,6 +495,8 @@ public class AutomotiveRepository { mediaItems.add(mediaItem); } + setPodcastEpisodesMetadata(episodes); + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); listenableFuture.set(libraryResult); @@ -422,6 +546,8 @@ public class AutomotiveRepository { mediaItems.add(mediaItem); } + setInternetRadioStationsMetadata(radioStations); + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); listenableFuture.set(libraryResult); @@ -452,29 +578,9 @@ public class AutomotiveRepository { List tracks = response.body().getSubsonicResponse().getAlbum().getSongs(); - List mediaItems = new ArrayList<>(); + setChildrenMetadata(tracks); - for (Child track : tracks) { - Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(track.getCoverArtId(), Preferences.getImageSize())); - - MediaMetadata mediaMetadata = new MediaMetadata.Builder() - .setTitle(track.getTitle()) - .setAlbumTitle(track.getAlbum()) - .setArtist(track.getArtist()) - .setIsBrowsable(false) - .setIsPlayable(true) - .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC) - .setArtworkUri(artworkUri) - .build(); - - MediaItem mediaItem = new MediaItem.Builder() - .setMediaId(track.getId()) - .setMediaMetadata(mediaMetadata) - .setUri(MusicUtil.getStreamUri(track.getId())) - .build(); - - mediaItems.add(mediaItem); - } + List mediaItems = MappingUtil.mapMediaItems(tracks); LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); @@ -486,10 +592,234 @@ public class AutomotiveRepository { @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - + listenableFuture.setException(t); } }); return listenableFuture; } + + public ListenableFuture>> getPlaylistSongs(String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getPlaylistClient() + .getPlaylist(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylist() != null && response.body().getSubsonicResponse().getPlaylist().getEntries() != null) { + List tracks = response.body().getSubsonicResponse().getPlaylist().getEntries(); + + setChildrenMetadata(tracks); + + List mediaItems = MappingUtil.mapMediaItems(tracks); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public void setChildrenMetadata(List children) { + long timestamp = System.currentTimeMillis(); + ArrayList sessionMediaItems = new ArrayList<>(); + + for (Child child : children) { + SessionMediaItem sessionMediaItem = new SessionMediaItem(child); + sessionMediaItem.setTimestamp(timestamp); + sessionMediaItems.add(sessionMediaItem); + } + + InsertAllThreadSafe insertAll = new InsertAllThreadSafe(sessionMediaItemDao, sessionMediaItems); + Thread thread = new Thread(insertAll); + thread.start(); + } + + public void setPodcastEpisodesMetadata(List podcastEpisodes) { + long timestamp = System.currentTimeMillis(); + ArrayList sessionMediaItems = new ArrayList<>(); + + for (PodcastEpisode podcastEpisode : podcastEpisodes) { + SessionMediaItem sessionMediaItem = new SessionMediaItem(podcastEpisode); + sessionMediaItem.setTimestamp(timestamp); + sessionMediaItems.add(sessionMediaItem); + } + + InsertAllThreadSafe insertAll = new InsertAllThreadSafe(sessionMediaItemDao, sessionMediaItems); + Thread thread = new Thread(insertAll); + thread.start(); + } + + public void setInternetRadioStationsMetadata(List internetRadioStations) { + long timestamp = System.currentTimeMillis(); + ArrayList sessionMediaItems = new ArrayList<>(); + + for (InternetRadioStation internetRadioStation : internetRadioStations) { + SessionMediaItem sessionMediaItem = new SessionMediaItem(internetRadioStation); + sessionMediaItem.setTimestamp(timestamp); + sessionMediaItems.add(sessionMediaItem); + } + + InsertAllThreadSafe insertAll = new InsertAllThreadSafe(sessionMediaItemDao, sessionMediaItems); + Thread thread = new Thread(insertAll); + thread.start(); + } + + public SessionMediaItem getSessionMediaItem(String id) { + SessionMediaItem sessionMediaItem = null; + + GetMediaItemThreadSafe getMediaItemThreadSafe = new GetMediaItemThreadSafe(sessionMediaItemDao, id); + Thread thread = new Thread(getMediaItemThreadSafe); + thread.start(); + + try { + thread.join(); + sessionMediaItem = getMediaItemThreadSafe.getSessionMediaItem(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return sessionMediaItem; + } + + public List getMetadatas(long timestamp) { + List mediaItems = Collections.emptyList(); + + GetMediaItemsThreadSafe getMediaItemsThreadSafe = new GetMediaItemsThreadSafe(sessionMediaItemDao, timestamp); + Thread thread = new Thread(getMediaItemsThreadSafe); + thread.start(); + + try { + thread.join(); + mediaItems = getMediaItemsThreadSafe.getMediaItems(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return mediaItems; + } + + public void deleteMetadata() { + DeleteAllThreadSafe delete = new DeleteAllThreadSafe(sessionMediaItemDao); + Thread thread = new Thread(delete); + thread.start(); + } + + private static class GetMediaItemThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + private final String id; + + private SessionMediaItem sessionMediaItem; + + public GetMediaItemThreadSafe(SessionMediaItemDao sessionMediaItemDao, String id) { + this.sessionMediaItemDao = sessionMediaItemDao; + this.id = id; + } + + @Override + public void run() { + sessionMediaItem = sessionMediaItemDao.get(id); + } + + public SessionMediaItem getSessionMediaItem() { + return sessionMediaItem; + } + } + + private static class GetMediaItemsThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + private final Long timestamp; + + private List mediaItems = new ArrayList<>(); + + public GetMediaItemsThreadSafe(SessionMediaItemDao sessionMediaItemDao, Long timestamp) { + this.sessionMediaItemDao = sessionMediaItemDao; + this.timestamp = timestamp; + } + + @Override + public void run() { + List sessionMediaItems = sessionMediaItemDao.get(timestamp); + sessionMediaItems.forEach(sessionMediaItem -> mediaItems.add(sessionMediaItem.getMediaItem())); + } + + public List getMediaItems() { + return mediaItems; + } + } + + /* private static class InsertThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + private final List children; + private final List podcastEpisodes; + private final List internetRadioStations; + private final long timestamp; + + public InsertThreadSafe(SessionMediaItemDao sessionMediaItemDao, List children, List podcastEpisodes, List internetRadioStations, long timestamp) { + this.sessionMediaItemDao = sessionMediaItemDao; + this.children = children; + this.podcastEpisodes = podcastEpisodes; + this.internetRadioStations = internetRadioStations; + this.timestamp = timestamp; + } + + @Override + public void run() { + if (children != null) { + SessionMediaItem sessionMediaItem = new SessionMediaItem(children); + sessionMediaItem.setTimestamp(timestamp); + sessionMediaItemDao.insert(sessionMediaItem); + } + + if (podcastEpisodes != null) { + SessionMediaItem sessionMediaItem = new SessionMediaItem(podcastEpisodes); + sessionMediaItem.setTimestamp(timestamp); + sessionMediaItemDao.insert(sessionMediaItem); + } + + if (internetRadioStations != null) { + SessionMediaItem sessionMediaItem = new SessionMediaItem(internetRadioStations); + sessionMediaItem.setTimestamp(timestamp); + sessionMediaItemDao.insert(sessionMediaItem); + } + } + } */ + + private static class InsertAllThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + private final List sessionMediaItems; + + public InsertAllThreadSafe(SessionMediaItemDao sessionMediaItemDao, List sessionMediaItems) { + this.sessionMediaItemDao = sessionMediaItemDao; + this.sessionMediaItems = sessionMediaItems; + } + + @Override + public void run() { + sessionMediaItemDao.insertAll(sessionMediaItems); + } + } + + private static class DeleteAllThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + + public DeleteAllThreadSafe(SessionMediaItemDao sessionMediaItemDao) { + this.sessionMediaItemDao = sessionMediaItemDao; + } + + @Override + public void run() { + sessionMediaItemDao.deleteAll(); + } + } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java index f94b84a9..3b43d30d 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java @@ -82,6 +82,8 @@ public class MappingUtil { .setArtist(MusicUtil.getReadableString(media.getArtist())) .setArtworkUri(artworkUri) .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) .build() ) .setRequestMetadata( @@ -116,6 +118,8 @@ public class MappingUtil { .setReleaseYear(media.getYear() != null ? media.getYear() : 0) .setAlbumTitle(MusicUtil.getReadableString(media.getAlbum())) .setArtist(MusicUtil.getReadableString(media.getArtist())) + .setIsBrowsable(false) + .setIsPlayable(true) .build() ) .setRequestMetadata( @@ -145,6 +149,8 @@ public class MappingUtil { .setTitle(internetRadioStation.getName()) .setArtist(internetRadioStation.getStreamUrl()) .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) .build() ) .setRequestMetadata( @@ -193,6 +199,8 @@ public class MappingUtil { .setArtist(MusicUtil.getReadableString(podcastEpisode.getArtist())) .setArtworkUri(artworkUri) .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) .build() ) .setRequestMetadata( @@ -201,12 +209,6 @@ public class MappingUtil { .setExtras(bundle) .build() ) - /* .setClippingConfiguration( - new MediaItem.ClippingConfiguration.Builder() - .setStartPositionMs(0) - .setEndPositionMs(podcastEpisode.getDuration() * 1000) - .build() - ) */ .setMimeType(MimeTypes.BASE_TYPE_AUDIO) .setUri(uri) .build(); diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt index d7353c90..82a1b6ef 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaBrowserTree.kt @@ -2,17 +2,19 @@ package com.cappielloantonio.tempo.service import android.content.Context import android.net.Uri +import android.util.Log import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem.SubtitleConfiguration import androidx.media3.common.MediaMetadata import androidx.media3.session.LibraryResult import androidx.media3.session.MediaLibraryService +import com.cappielloantonio.tempo.model.SessionMediaItem import com.cappielloantonio.tempo.repository.AutomotiveRepository -import com.cappielloantonio.tempo.util.MusicUtil 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 +import java.lang.Exception object MediaBrowserTree { @@ -42,6 +44,8 @@ object MediaBrowserTree { // 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 @@ -343,7 +347,7 @@ object MediaBrowserTree { RECENTLY_ADDED_ID -> automotiveRepository.getAlbums(id, "newest", 100) BEST_OF_ID -> automotiveRepository.getStarredArtists(id, true) MADE_FOR_YOU_ID -> automotiveRepository.getStarredArtists(id, true) - STARRED_TRACKS_ID -> automotiveRepository.getStarredSongs(id) + STARRED_TRACKS_ID -> automotiveRepository.starredSongs STARRED_ALBUMS_ID -> automotiveRepository.getStarredAlbums(id) STARRED_ARTISTS_ID -> automotiveRepository.getStarredArtists(id, false) FOLDER_ID -> automotiveRepository.getMusicFolders(id) @@ -359,7 +363,6 @@ object MediaBrowserTree { id.removePrefix( MOST_PLAYED_ID ) - ) } @@ -368,7 +371,6 @@ object MediaBrowserTree { id.removePrefix( LAST_PLAYED_ID ) - ) } @@ -377,7 +379,6 @@ object MediaBrowserTree { id.removePrefix( RECENTLY_ADDED_ID ) - ) } @@ -390,11 +391,38 @@ object MediaBrowserTree { } 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(PODCAST_ID)) { @@ -410,28 +438,27 @@ object MediaBrowserTree { } } - fun getItem(id: String): ListenableFuture> { - val mediaMetadata = MediaMetadata.Builder() - .setTitle("Titolo") - .setAlbumTitle("Titolo album") - .setArtist("Artista") - .setIsBrowsable(false) - .setIsPlayable(true) - .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC) - .build() + // https://github.com/androidx/media/issues/156 + fun getItems(mediaItems: List): List { + val updatedMediaItems = ArrayList() - val mediaItem = MediaItem.Builder() - .setMediaId(id) - .setMediaMetadata(mediaMetadata) - .setUri(MusicUtil.getStreamUri(id)) - .build() + mediaItems.forEach { + if (it.localConfiguration?.uri != null) { + updatedMediaItems.add(it) + } else { + val sessionMediaItem = automotiveRepository.getSessionMediaItem(it.mediaId) - return Futures.immediateFuture(LibraryResult.ofItem(mediaItem, null)) + if (sessionMediaItem != null) { + var toAdd = automotiveRepository.getMetadatas(sessionMediaItem.timestamp!!) + val index = toAdd.indexOfFirst { mediaItem -> mediaItem.mediaId == it.mediaId } - /* if (treeNodes[id]?.item != null) { - return Futures.immediateFuture(LibraryResult.ofItem(treeNodes[id]!!.item, null)) + toAdd = toAdd.subList(index, toAdd.size) + + updatedMediaItems.addAll(toAdd) + } + } } - return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)) */ + return updatedMediaItems } } diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt index 2cf347de..e9f67e45 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt @@ -121,91 +121,17 @@ open class MediaLibrarySessionCallback( return MediaBrowserTree.getChildren(parentId, params) } - /* override fun onGetItem( - session: MediaLibraryService.MediaLibrarySession, - browser: MediaSession.ControllerInfo, - mediaId: String - ): ListenableFuture> { - Log.d(TAG, "onGetItem()") - - return MediaBrowserTree.getItem(mediaId) - } */ - - /* override fun onAddMediaItems( + override fun onAddMediaItems( mediaSession: MediaSession, controller: MediaSession.ControllerInfo, mediaItems: List ): ListenableFuture> { - Log.d(TAG, "onAddMediaItems()") - - return Futures.immediateFuture(mediaItems) - } - - @OptIn(UnstableApi::class) - override fun onSetMediaItems( - mediaSession: MediaSession, - browser: MediaSession.ControllerInfo, - mediaItems: List, - startIndex: Int, - startPositionMs: Long - ): ListenableFuture { - Log.d(TAG, "onSetMediaItems()") - - val mediaItemss: MutableList = ArrayList() - - val mediaMetadata = MediaMetadata.Builder() - .setTitle("Titolo") - .setAlbumTitle("Titolo album") - .setArtist("Artista") - .setIsBrowsable(false) - .setIsPlayable(true) - .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC) - .build() - - val mediaItem = MediaItem.Builder() - .setMediaId(mediaItems.get(0).mediaId) - .setMediaMetadata(mediaMetadata) - .setUri(MusicUtil.getStreamUri(mediaItems.get(0).mediaId)) - .build() - - mediaItemss.add(mediaItem) - - return Futures.immediateFuture( - MediaSession.MediaItemsWithStartPosition( - mediaItemss, 0, 0 - ) + return super.onAddMediaItems( + mediaSession, + controller, + MediaBrowserTree.getItems(mediaItems) ) - } */ - - /* @OptIn(UnstableApi::class) // MediaSession.MediaItemsWithStartPosition - private fun maybeExpandSingleItemToPlaylist( - mediaItem: MediaItem, startIndex: Int, startPositionMs: Long - ): MediaSession.MediaItemsWithStartPosition? { - var playlist = listOf() - var indexInPlaylist = startIndex - - MediaBrowserTree.getItem(mediaItem.mediaId)?.apply { - if (mediaMetadata.isBrowsable == true) { - playlist = MediaBrowserTree.getChildren(mediaId) - } else if (requestMetadata.searchQuery == null) { - MediaBrowserTree.getParentId(mediaId)?.let { - playlist = MediaBrowserTree.getChildren(it).map { mediaItem -> - if (mediaItem.mediaId == mediaId) MediaBrowserTree.expandItem(mediaItem)!! else mediaItem - } - - indexInPlaylist = MediaBrowserTree.getIndexInMediaItems(mediaId, playlist) - } - } - } - - if (playlist.isNotEmpty()) { - return MediaSession.MediaItemsWithStartPosition( - playlist, indexInPlaylist, startPositionMs - ) - } - - return null - } */ + } /* override fun onSearch( session: MediaLibraryService.MediaLibrarySession, diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt index 3791393f..3b414a4e 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -185,6 +185,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { if (this::castPlayer.isInitialized) castPlayer.release() player.release() mediaLibrarySession.release() + automotiveRepository.deleteMetadata() clearListener() }