package com.cappielloantonio.tempo.service; import android.content.ComponentName; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.media3.common.MediaItem; import androidx.media3.common.Player; import androidx.media3.common.Timeline; import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaBrowser; import androidx.media3.session.SessionToken; import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.interfaces.MediaIndexCallback; import com.cappielloantonio.tempo.model.Chronology; import com.cappielloantonio.tempo.repository.ChronologyRepository; import com.cappielloantonio.tempo.repository.QueueRepository; import com.cappielloantonio.tempo.repository.SongRepository; 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.MappingUtil; import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.lang.ref.WeakReference; import java.util.List; import java.util.concurrent.ExecutionException; public class MediaManager { private static final String TAG = "MediaManager"; private static WeakReference attachedBrowserRef = new WeakReference<>(null); public static void registerPlaybackObserver( ListenableFuture browserFuture, PlaybackViewModel playbackViewModel ) { if (browserFuture == null) return; Futures.addCallback(browserFuture, new FutureCallback() { @Override public void onSuccess(MediaBrowser browser) { MediaBrowser current = attachedBrowserRef.get(); if (current != browser) { browser.addListener(new Player.Listener() { @Override public void onEvents(@NonNull Player player, @NonNull Player.Events events) { if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION) || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED) || events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) { String mediaId = player.getCurrentMediaItem() != null ? player.getCurrentMediaItem().mediaId : null; boolean playing = player.getPlaybackState() == Player.STATE_READY && player.getPlayWhenReady(); playbackViewModel.update(mediaId, playing); } } }); String mediaId = browser.getCurrentMediaItem() != null ? browser.getCurrentMediaItem().mediaId : null; boolean playing = browser.getPlaybackState() == Player.STATE_READY && browser.getPlayWhenReady(); playbackViewModel.update(mediaId, playing); attachedBrowserRef = new WeakReference<>(browser); } else { String mediaId = browser.getCurrentMediaItem() != null ? browser.getCurrentMediaItem().mediaId : null; boolean playing = browser.getPlaybackState() == Player.STATE_READY && browser.getPlayWhenReady(); playbackViewModel.update(mediaId, playing); } } @Override public void onFailure(@NonNull Throwable t) { Log.e(TAG, "Failed to get MediaBrowser instance", t); } }, MoreExecutors.directExecutor()); } public static void onBrowserReleased(@Nullable MediaBrowser released) { MediaBrowser attached = attachedBrowserRef.get(); if (attached == released) { attachedBrowserRef.clear(); } } public static void reset(ListenableFuture mediaBrowserListenableFuture) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.get().isPlaying()) { mediaBrowserListenableFuture.get().pause(); } mediaBrowserListenableFuture.get().stop(); mediaBrowserListenableFuture.get().clearMediaItems(); clearDatabase(); } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void hide(ListenableFuture mediaBrowserListenableFuture) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.get().isPlaying()) { mediaBrowserListenableFuture.get().pause(); } } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void check(ListenableFuture mediaBrowserListenableFuture) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.get().getMediaItemCount() < 1) { List media = getQueueRepository().getMedia(); if (media != null && media.size() >= 1) { init(mediaBrowserListenableFuture, media); } } } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void init(ListenableFuture mediaBrowserListenableFuture, List media) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { mediaBrowserListenableFuture.get().clearMediaItems(); mediaBrowserListenableFuture.get().setMediaItems(MappingUtil.mapMediaItems(media)); mediaBrowserListenableFuture.get().seekTo(getQueueRepository().getLastPlayedMediaIndex(), getQueueRepository().getLastPlayedMediaTimestamp()); mediaBrowserListenableFuture.get().prepare(); } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void startQueue(ListenableFuture mediaBrowserListenableFuture, List media, int startIndex) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { MediaBrowser browser = mediaBrowserListenableFuture.get(); browser.clearMediaItems(); browser.setMediaItems(MappingUtil.mapMediaItems(media)); browser.prepare(); Player.Listener timelineListener = new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { int itemCount = browser.getMediaItemCount(); if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) { browser.seekTo(startIndex, 0); browser.play(); browser.removeListener(this); } } }; browser.addListener(timelineListener); enqueueDatabase(media, true, 0); } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void startQueue(ListenableFuture mediaBrowserListenableFuture, Child media) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { mediaBrowserListenableFuture.get().clearMediaItems(); mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(media)); mediaBrowserListenableFuture.get().prepare(); mediaBrowserListenableFuture.get().play(); enqueueDatabase(media, true, 0); } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void playDownloadedMediaItem(ListenableFuture mediaBrowserListenableFuture, MediaItem mediaItem) { if (mediaBrowserListenableFuture != null && mediaItem != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); mediaBrowser.clearMediaItems(); mediaBrowser.setMediaItem(mediaItem); mediaBrowser.prepare(); mediaBrowser.play(); clearDatabase(); } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void startRadio(ListenableFuture mediaBrowserListenableFuture, InternetRadioStation internetRadioStation) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { mediaBrowserListenableFuture.get().clearMediaItems(); mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation)); mediaBrowserListenableFuture.get().prepare(); mediaBrowserListenableFuture.get().play(); } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void startPodcast(ListenableFuture mediaBrowserListenableFuture, PodcastEpisode podcastEpisode) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { mediaBrowserListenableFuture.get().clearMediaItems(); mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(podcastEpisode)); mediaBrowserListenableFuture.get().prepare(); mediaBrowserListenableFuture.get().play(); } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void enqueue(ListenableFuture mediaBrowserListenableFuture, List media, boolean playImmediatelyAfter) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { if (playImmediatelyAfter && mediaBrowserListenableFuture.get().getNextMediaItemIndex() != -1) { enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getNextMediaItemIndex()); mediaBrowserListenableFuture.get().addMediaItems(mediaBrowserListenableFuture.get().getNextMediaItemIndex(), MappingUtil.mapMediaItems(media)); } else { enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount()); mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media)); } } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void enqueue(ListenableFuture mediaBrowserListenableFuture, Child media, boolean playImmediatelyAfter) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { if (playImmediatelyAfter && mediaBrowserListenableFuture.get().getNextMediaItemIndex() != -1) { enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getNextMediaItemIndex()); mediaBrowserListenableFuture.get().addMediaItem(mediaBrowserListenableFuture.get().getNextMediaItemIndex(), MappingUtil.mapMediaItem(media)); } else { enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount()); mediaBrowserListenableFuture.get().addMediaItem(MappingUtil.mapMediaItem(media)); } } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void shuffle(ListenableFuture mediaBrowserListenableFuture, List media, int startIndex, int endIndex) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { mediaBrowserListenableFuture.get().removeMediaItems(startIndex, endIndex + 1); mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media).subList(startIndex, endIndex + 1)); swapDatabase(media); } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void swap(ListenableFuture mediaBrowserListenableFuture, List media, int from, int to) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { mediaBrowserListenableFuture.get().moveMediaItem(from, to); swapDatabase(media); } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void remove(ListenableFuture mediaBrowserListenableFuture, List media, int toRemove) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.get().getMediaItemCount() > 1 && mediaBrowserListenableFuture.get().getCurrentMediaItemIndex() != toRemove) { mediaBrowserListenableFuture.get().removeMediaItem(toRemove); removeDatabase(media, toRemove); } else { removeDatabase(media, -1); } } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void removeRange(ListenableFuture mediaBrowserListenableFuture, List media, int fromItem, int toItem) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { mediaBrowserListenableFuture.get().removeMediaItems(fromItem, toItem); removeRangeDatabase(media, fromItem, toItem); } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void getCurrentIndex(ListenableFuture mediaBrowserListenableFuture, MediaIndexCallback callback) { if (mediaBrowserListenableFuture != null) { mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { callback.onRecovery(mediaBrowserListenableFuture.get().getCurrentMediaItemIndex()); } } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, MoreExecutors.directExecutor()); } } public static void setLastPlayedTimestamp(MediaItem mediaItem) { if (mediaItem != null) getQueueRepository().setLastPlayedTimestamp(mediaItem.mediaId); } public static void setPlayingPausedTimestamp(MediaItem mediaItem, long ms) { if (mediaItem != null) getQueueRepository().setPlayingPausedTimestamp(mediaItem.mediaId, ms); } public static void scrobble(MediaItem mediaItem, boolean submission) { if (mediaItem != null && Preferences.isScrobblingEnabled()) { getSongRepository().scrobble(mediaItem.mediaMetadata.extras.getString("id"), submission); } } @OptIn(markerClass = UnstableApi.class) public static void continuousPlay(MediaItem mediaItem) { if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) { Preferences.setLastInstantMix(); LiveData> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, 10); instantMix.observeForever(new Observer>() { @Override public void onChanged(List media) { if (media != null) { ListenableFuture mediaBrowserListenableFuture = new MediaBrowser.Builder( App.getContext(), new SessionToken(App.getContext(), new ComponentName(App.getContext(), MediaService.class)) ).buildAsync(); enqueue(mediaBrowserListenableFuture, media, true); } instantMix.removeObserver(this); } }); } } public static void saveChronology(MediaItem mediaItem) { if (mediaItem != null) { getChronologyRepository().insert(new Chronology(mediaItem)); } } private static QueueRepository getQueueRepository() { return new QueueRepository(); } private static SongRepository getSongRepository() { return new SongRepository(); } private static ChronologyRepository getChronologyRepository() { return new ChronologyRepository(); } private static void enqueueDatabase(List media, boolean reset, int afterIndex) { getQueueRepository().insertAll(media, reset, afterIndex); } private static void enqueueDatabase(Child media, boolean reset, int afterIndex) { getQueueRepository().insert(media, reset, afterIndex); } private static void swapDatabase(List media) { getQueueRepository().insertAll(media, true, 0); } private static void removeDatabase(List media, int toRemove) { if (toRemove != -1) { media.remove(toRemove); getQueueRepository().insertAll(media, true, 0); } } private static void removeRangeDatabase(List media, int fromItem, int toItem) { List toRemove = media.subList(fromItem, toItem); media.removeAll(toRemove); getQueueRepository().insertAll(media, true, 0); } public static void clearDatabase() { getQueueRepository().deleteAll(); } }