feat: test: Implemented initial functional version with Android Auto support

This commit is contained in:
antonio 2024-01-03 00:45:22 +01:00
parent e8c7c065e2
commit d6cc4fc028
8 changed files with 758 additions and 160 deletions

View file

@ -14,17 +14,19 @@ import com.cappielloantonio.tempo.database.dao.FavoriteDao;
import com.cappielloantonio.tempo.database.dao.QueueDao; import com.cappielloantonio.tempo.database.dao.QueueDao;
import com.cappielloantonio.tempo.database.dao.RecentSearchDao; import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
import com.cappielloantonio.tempo.database.dao.ServerDao; 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.Chronology;
import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.Favorite; import com.cappielloantonio.tempo.model.Favorite;
import com.cappielloantonio.tempo.model.Queue; import com.cappielloantonio.tempo.model.Queue;
import com.cappielloantonio.tempo.model.RecentSearch; import com.cappielloantonio.tempo.model.RecentSearch;
import com.cappielloantonio.tempo.model.Server; import com.cappielloantonio.tempo.model.Server;
import com.cappielloantonio.tempo.model.SessionMediaItem;
@Database( @Database(
version = 3, version = 8,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class}, entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class},
autoMigrations = {@AutoMigration(from = 2, to = 3)} autoMigrations = {@AutoMigration(from = 7, to = 8)}
) )
@TypeConverters({DateConverters.class}) @TypeConverters({DateConverters.class})
public abstract class AppDatabase extends RoomDatabase { public abstract class AppDatabase extends RoomDatabase {
@ -52,4 +54,6 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract ChronologyDao chronologyDao(); public abstract ChronologyDao chronologyDao();
public abstract FavoriteDao favoriteDao(); public abstract FavoriteDao favoriteDao();
public abstract SessionMediaItemDao sessionMediaItemDao();
} }

View file

@ -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<SessionMediaItem> get(long timestamp);
@Insert(onConflict = OnConflictStrategy.IGNORE)
void insert(SessionMediaItem sessionMediaItem);
@Insert(onConflict = OnConflictStrategy.IGNORE)
void insertAll(List<SessionMediaItem> sessionMediaItems);
@Query("DELETE FROM session_media_item")
void deleteAll();
}

View file

@ -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)
}
}
}
}

View file

@ -9,15 +9,22 @@ import androidx.media3.common.MediaMetadata;
import androidx.media3.session.LibraryResult; import androidx.media3.session.LibraryResult;
import com.cappielloantonio.tempo.App; 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.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.SessionMediaItem;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3; 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.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child; 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.InternetRadioStation;
import com.cappielloantonio.tempo.subsonic.models.MusicFolder; import com.cappielloantonio.tempo.subsonic.models.MusicFolder;
import com.cappielloantonio.tempo.subsonic.models.Playlist; import com.cappielloantonio.tempo.subsonic.models.Playlist;
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
import com.google.common.collect.ImmutableList; 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 com.google.common.util.concurrent.SettableFuture;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
public class AutomotiveRepository { public class AutomotiveRepository {
private final SessionMediaItemDao sessionMediaItemDao = AppDatabase.getInstance().sessionMediaItemDao();
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getAlbums(String prefix, String type, int size) { public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getAlbums(String prefix, String type, int size) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create(); final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
@ -86,7 +97,7 @@ public class AutomotiveRepository {
return listenableFuture; return listenableFuture;
} }
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getStarredSongs(String prefix) { public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getStarredSongs() {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create(); final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false) 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) { if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null && response.body().getSubsonicResponse().getStarred2().getSongs() != null) {
List<Child> songs = response.body().getSubsonicResponse().getStarred2().getSongs(); List<Child> songs = response.body().getSubsonicResponse().getStarred2().getSongs();
List<MediaItem> mediaItems = new ArrayList<>(); setChildrenMetadata(songs);
for (Child song : songs) { List<MediaItem> mediaItems = MappingUtil.mapMediaItems(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);
}
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
@ -252,7 +243,7 @@ public class AutomotiveRepository {
.enqueue(new Callback<ApiResponse>() { .enqueue(new Callback<ApiResponse>() {
@Override @Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) { public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> 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<MusicFolder> musicFolders = response.body().getSubsonicResponse().getMusicFolders().getMusicFolders(); List<MusicFolder> musicFolders = response.body().getSubsonicResponse().getMusicFolders().getMusicFolders();
List<MediaItem> mediaItems = new ArrayList<>(); List<MediaItem> mediaItems = new ArrayList<>();
@ -291,6 +282,137 @@ public class AutomotiveRepository {
return listenableFuture; return listenableFuture;
} }
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getIndexes(String prefix, String id) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getIndexes(id, null)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getIndexes() != null) {
List<MediaItem> mediaItems = new ArrayList<>();
if (response.body().getSubsonicResponse().getIndexes().getIndices() != null) {
List<Index> 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<Child> 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<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getDirectories(String prefix, String id) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getMusicDirectory(id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> 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<MediaItem> 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<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getPlaylists(String prefix) { public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getPlaylists(String prefix) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create(); final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
@ -373,6 +495,8 @@ public class AutomotiveRepository {
mediaItems.add(mediaItem); mediaItems.add(mediaItem);
} }
setPodcastEpisodesMetadata(episodes);
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult); listenableFuture.set(libraryResult);
@ -422,6 +546,8 @@ public class AutomotiveRepository {
mediaItems.add(mediaItem); mediaItems.add(mediaItem);
} }
setInternetRadioStationsMetadata(radioStations);
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult); listenableFuture.set(libraryResult);
@ -452,29 +578,9 @@ public class AutomotiveRepository {
List<Child> tracks = response.body().getSubsonicResponse().getAlbum().getSongs(); List<Child> tracks = response.body().getSubsonicResponse().getAlbum().getSongs();
List<MediaItem> mediaItems = new ArrayList<>(); setChildrenMetadata(tracks);
for (Child track : tracks) { List<MediaItem> mediaItems = MappingUtil.mapMediaItems(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);
}
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
@ -486,10 +592,234 @@ public class AutomotiveRepository {
@Override @Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) { public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
} }
}); });
return listenableFuture; return listenableFuture;
} }
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getPlaylistSongs(String id) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getPlaylistClient()
.getPlaylist(id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylist() != null && response.body().getSubsonicResponse().getPlaylist().getEntries() != null) {
List<Child> tracks = response.body().getSubsonicResponse().getPlaylist().getEntries();
setChildrenMetadata(tracks);
List<MediaItem> mediaItems = MappingUtil.mapMediaItems(tracks);
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public void setChildrenMetadata(List<Child> children) {
long timestamp = System.currentTimeMillis();
ArrayList<SessionMediaItem> 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<PodcastEpisode> podcastEpisodes) {
long timestamp = System.currentTimeMillis();
ArrayList<SessionMediaItem> 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<InternetRadioStation> internetRadioStations) {
long timestamp = System.currentTimeMillis();
ArrayList<SessionMediaItem> 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<MediaItem> getMetadatas(long timestamp) {
List<MediaItem> 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<MediaItem> mediaItems = new ArrayList<>();
public GetMediaItemsThreadSafe(SessionMediaItemDao sessionMediaItemDao, Long timestamp) {
this.sessionMediaItemDao = sessionMediaItemDao;
this.timestamp = timestamp;
}
@Override
public void run() {
List<SessionMediaItem> sessionMediaItems = sessionMediaItemDao.get(timestamp);
sessionMediaItems.forEach(sessionMediaItem -> mediaItems.add(sessionMediaItem.getMediaItem()));
}
public List<MediaItem> getMediaItems() {
return mediaItems;
}
}
/* private static class InsertThreadSafe implements Runnable {
private final SessionMediaItemDao sessionMediaItemDao;
private final List<Child> children;
private final List<PodcastEpisode> podcastEpisodes;
private final List<InternetRadioStation> internetRadioStations;
private final long timestamp;
public InsertThreadSafe(SessionMediaItemDao sessionMediaItemDao, List<Child> children, List<PodcastEpisode> podcastEpisodes, List<InternetRadioStation> 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<SessionMediaItem> sessionMediaItems;
public InsertAllThreadSafe(SessionMediaItemDao sessionMediaItemDao, List<SessionMediaItem> 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();
}
}
} }

View file

@ -82,6 +82,8 @@ public class MappingUtil {
.setArtist(MusicUtil.getReadableString(media.getArtist())) .setArtist(MusicUtil.getReadableString(media.getArtist()))
.setArtworkUri(artworkUri) .setArtworkUri(artworkUri)
.setExtras(bundle) .setExtras(bundle)
.setIsBrowsable(false)
.setIsPlayable(true)
.build() .build()
) )
.setRequestMetadata( .setRequestMetadata(
@ -116,6 +118,8 @@ public class MappingUtil {
.setReleaseYear(media.getYear() != null ? media.getYear() : 0) .setReleaseYear(media.getYear() != null ? media.getYear() : 0)
.setAlbumTitle(MusicUtil.getReadableString(media.getAlbum())) .setAlbumTitle(MusicUtil.getReadableString(media.getAlbum()))
.setArtist(MusicUtil.getReadableString(media.getArtist())) .setArtist(MusicUtil.getReadableString(media.getArtist()))
.setIsBrowsable(false)
.setIsPlayable(true)
.build() .build()
) )
.setRequestMetadata( .setRequestMetadata(
@ -145,6 +149,8 @@ public class MappingUtil {
.setTitle(internetRadioStation.getName()) .setTitle(internetRadioStation.getName())
.setArtist(internetRadioStation.getStreamUrl()) .setArtist(internetRadioStation.getStreamUrl())
.setExtras(bundle) .setExtras(bundle)
.setIsBrowsable(false)
.setIsPlayable(true)
.build() .build()
) )
.setRequestMetadata( .setRequestMetadata(
@ -193,6 +199,8 @@ public class MappingUtil {
.setArtist(MusicUtil.getReadableString(podcastEpisode.getArtist())) .setArtist(MusicUtil.getReadableString(podcastEpisode.getArtist()))
.setArtworkUri(artworkUri) .setArtworkUri(artworkUri)
.setExtras(bundle) .setExtras(bundle)
.setIsBrowsable(false)
.setIsPlayable(true)
.build() .build()
) )
.setRequestMetadata( .setRequestMetadata(
@ -201,12 +209,6 @@ public class MappingUtil {
.setExtras(bundle) .setExtras(bundle)
.build() .build()
) )
/* .setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(0)
.setEndPositionMs(podcastEpisode.getDuration() * 1000)
.build()
) */
.setMimeType(MimeTypes.BASE_TYPE_AUDIO) .setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setUri(uri) .setUri(uri)
.build(); .build();

View file

@ -2,17 +2,19 @@ package com.cappielloantonio.tempo.service
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.SubtitleConfiguration import androidx.media3.common.MediaItem.SubtitleConfiguration
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import androidx.media3.session.LibraryResult import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService
import com.cappielloantonio.tempo.model.SessionMediaItem
import com.cappielloantonio.tempo.repository.AutomotiveRepository import com.cappielloantonio.tempo.repository.AutomotiveRepository
import com.cappielloantonio.tempo.util.MusicUtil
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import java.lang.Exception
object MediaBrowserTree { object MediaBrowserTree {
@ -42,6 +44,8 @@ object MediaBrowserTree {
// Second level LIBRARY_ID // Second level LIBRARY_ID
private const val FOLDER_ID = "[folderID]" private const val FOLDER_ID = "[folderID]"
private const val INDEX_ID = "[indexID]"
private const val DIRECTORY_ID = "[directoryID]"
private const val PLAYLIST_ID = "[playlistID]" private const val PLAYLIST_ID = "[playlistID]"
// Second level OTHER_ID // Second level OTHER_ID
@ -343,7 +347,7 @@ object MediaBrowserTree {
RECENTLY_ADDED_ID -> automotiveRepository.getAlbums(id, "newest", 100) RECENTLY_ADDED_ID -> automotiveRepository.getAlbums(id, "newest", 100)
BEST_OF_ID -> automotiveRepository.getStarredArtists(id, true) BEST_OF_ID -> automotiveRepository.getStarredArtists(id, true)
MADE_FOR_YOU_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_ALBUMS_ID -> automotiveRepository.getStarredAlbums(id)
STARRED_ARTISTS_ID -> automotiveRepository.getStarredArtists(id, false) STARRED_ARTISTS_ID -> automotiveRepository.getStarredArtists(id, false)
FOLDER_ID -> automotiveRepository.getMusicFolders(id) FOLDER_ID -> automotiveRepository.getMusicFolders(id)
@ -359,7 +363,6 @@ object MediaBrowserTree {
id.removePrefix( id.removePrefix(
MOST_PLAYED_ID MOST_PLAYED_ID
) )
) )
} }
@ -368,7 +371,6 @@ object MediaBrowserTree {
id.removePrefix( id.removePrefix(
LAST_PLAYED_ID LAST_PLAYED_ID
) )
) )
} }
@ -377,7 +379,6 @@ object MediaBrowserTree {
id.removePrefix( id.removePrefix(
RECENTLY_ADDED_ID RECENTLY_ADDED_ID
) )
) )
} }
@ -390,11 +391,38 @@ object MediaBrowserTree {
} }
if (id.startsWith(FOLDER_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)) { if (id.startsWith(PLAYLIST_ID)) {
return automotiveRepository.getPlaylistSongs(
id.removePrefix(
PLAYLIST_ID
)
)
} }
if (id.startsWith(PODCAST_ID)) { if (id.startsWith(PODCAST_ID)) {
@ -410,28 +438,27 @@ object MediaBrowserTree {
} }
} }
fun getItem(id: String): ListenableFuture<LibraryResult<MediaItem>> { // https://github.com/androidx/media/issues/156
val mediaMetadata = MediaMetadata.Builder() fun getItems(mediaItems: List<MediaItem>): List<MediaItem> {
.setTitle("Titolo") val updatedMediaItems = ArrayList<MediaItem>()
.setAlbumTitle("Titolo album")
.setArtist("Artista")
.setIsBrowsable(false)
.setIsPlayable(true)
.setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
.build()
val mediaItem = MediaItem.Builder() mediaItems.forEach {
.setMediaId(id) if (it.localConfiguration?.uri != null) {
.setMediaMetadata(mediaMetadata) updatedMediaItems.add(it)
.setUri(MusicUtil.getStreamUri(id)) } else {
.build() 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) { toAdd = toAdd.subList(index, toAdd.size)
return Futures.immediateFuture(LibraryResult.ofItem(treeNodes[id]!!.item, null))
updatedMediaItems.addAll(toAdd)
}
}
} }
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)) */ return updatedMediaItems
} }
} }

View file

@ -121,91 +121,17 @@ open class MediaLibrarySessionCallback(
return MediaBrowserTree.getChildren(parentId, params) return MediaBrowserTree.getChildren(parentId, params)
} }
/* override fun onGetItem( override fun onAddMediaItems(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
mediaId: String
): ListenableFuture<LibraryResult<MediaItem>> {
Log.d(TAG, "onGetItem()")
return MediaBrowserTree.getItem(mediaId)
} */
/* override fun onAddMediaItems(
mediaSession: MediaSession, mediaSession: MediaSession,
controller: MediaSession.ControllerInfo, controller: MediaSession.ControllerInfo,
mediaItems: List<MediaItem> mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> { ): ListenableFuture<List<MediaItem>> {
Log.d(TAG, "onAddMediaItems()") return super.onAddMediaItems(
mediaSession,
return Futures.immediateFuture(mediaItems) controller,
} MediaBrowserTree.getItems(mediaItems)
@OptIn(UnstableApi::class)
override fun onSetMediaItems(
mediaSession: MediaSession,
browser: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>,
startIndex: Int,
startPositionMs: Long
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
Log.d(TAG, "onSetMediaItems()")
val mediaItemss: MutableList<MediaItem> = 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
)
) )
} */ }
/* @OptIn(UnstableApi::class) // MediaSession.MediaItemsWithStartPosition
private fun maybeExpandSingleItemToPlaylist(
mediaItem: MediaItem, startIndex: Int, startPositionMs: Long
): MediaSession.MediaItemsWithStartPosition? {
var playlist = listOf<MediaItem>()
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( /* override fun onSearch(
session: MediaLibraryService.MediaLibrarySession, session: MediaLibraryService.MediaLibrarySession,

View file

@ -185,6 +185,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
if (this::castPlayer.isInitialized) castPlayer.release() if (this::castPlayer.isInitialized) castPlayer.release()
player.release() player.release()
mediaLibrarySession.release() mediaLibrarySession.release()
automotiveRepository.deleteMetadata()
clearListener() clearListener()
} }