diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java deleted file mode 100644 index f91193d2..00000000 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ /dev/null @@ -1,524 +0,0 @@ -package com.cappielloantonio.tempo.service; - -import android.content.ComponentName; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.OptIn; -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.Constants.SeedType; -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; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; - -public class MediaManager { - private static final String TAG = "MediaManager"; - private static WeakReference attachedBrowserRef = new WeakReference<>(null); - public static AtomicBoolean justStarted = new AtomicBoolean(false); - - private static final ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor(); - - 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()); - } - } - - @OptIn(markerClass = UnstableApi.class) - public static void startQueue(ListenableFuture mediaBrowserListenableFuture, List media, int startIndex) { - if (mediaBrowserListenableFuture != null) { - - mediaBrowserListenableFuture.addListener(() -> { - try { - if (mediaBrowserListenableFuture.isDone()) { - final MediaBrowser browser = mediaBrowserListenableFuture.get(); - final List items = MappingUtil.mapMediaItems(media); - - new Handler(Looper.getMainLooper()).post(() -> { - justStarted.set(true); - browser.setMediaItems(items, startIndex, 0); - 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); - } else { - Log.d(TAG, "Cannot start playback: itemCount=" + itemCount + ", startIndex=" + startIndex); - } - } - }; - - browser.addListener(timelineListener); - }); - - backgroundExecutor.execute(() -> { - Log.d(TAG, "Background: enqueuing to database"); - enqueueDatabase(media, true, 0); - }); - } - } catch (ExecutionException | InterruptedException e) { - Log.e(TAG, "Error in startQueue: " + e.getMessage(), e); - } - }, MoreExecutors.directExecutor()); - } - } - - public static void startQueue(ListenableFuture mediaBrowserListenableFuture, Child media) { - if (mediaBrowserListenableFuture != null) { - mediaBrowserListenableFuture.addListener(() -> { - try { - if (mediaBrowserListenableFuture.isDone()) { - MediaBrowser browser = mediaBrowserListenableFuture.get(); - justStarted.set(true); - browser.setMediaItem(MappingUtil.mapMediaItem(media)); - browser.prepare(); - browser.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(); - justStarted.set(true); - 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()) { - MediaBrowser browser = mediaBrowserListenableFuture.get(); - justStarted.set(true); - browser.setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation)); - browser.prepare(); - browser.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()) { - MediaBrowser browser = mediaBrowserListenableFuture.get(); - justStarted.set(true); - browser.setMediaItem(MappingUtil.mapMediaItem(podcastEpisode)); - browser.prepare(); - browser.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()) { - Log.e(TAG, "enqueue"); - MediaBrowser browser = mediaBrowserListenableFuture.get(); - if (playImmediatelyAfter && browser.getNextMediaItemIndex() != -1) { - enqueueDatabase(media, false, browser.getNextMediaItemIndex()); - browser.addMediaItems(browser.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()) { - Log.e(TAG, "enqueue"); - MediaBrowser browser = mediaBrowserListenableFuture.get(); - if (playImmediatelyAfter && browser.getNextMediaItemIndex() != -1) { - enqueueDatabase(media, false, browser.getNextMediaItemIndex()); - browser.addMediaItem(browser.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()) { - Log.e(TAG, "shuffle"); - MediaBrowser browser = mediaBrowserListenableFuture.get(); - browser.removeMediaItems(startIndex, endIndex + 1); - browser.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()) { - Log.e(TAG, "swap"); - 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()) { - Log.e(TAG, "remove"); - 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()) { - Log.e(TAG, "remove range"); - 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, - ListenableFuture existingBrowserFuture) { - if (mediaItem == null - || !Preferences.isContinuousPlayEnabled() - || !Preferences.isInstantMixUsable()) { - return; - } - - Preferences.setLastInstantMix(); - - LiveData> instantMix = - getSongRepository().getContinuousMix(mediaItem.mediaId, 25); - - instantMix.observeForever(new Observer>() { - @Override - public void onChanged(List media) { - if (media == null || media.isEmpty()) { - return; - } - - if (existingBrowserFuture != null) { - Log.d(TAG, "Continuous play: adding " + media.size() + " tracks"); - enqueue(existingBrowserFuture, 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(); - } -} diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.kt b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.kt new file mode 100644 index 00000000..375560b9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.kt @@ -0,0 +1,475 @@ +package com.cappielloantonio.tempo.service + +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.lifecycle.Observer +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.Timeline +import androidx.media3.session.MediaBrowser +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.isContinuousPlayEnabled +import com.cappielloantonio.tempo.util.Preferences.isInstantMixUsable +import com.cappielloantonio.tempo.util.Preferences.isScrobblingEnabled +import com.cappielloantonio.tempo.util.Preferences.setLastInstantMix +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.MoreExecutors +import java.lang.ref.WeakReference +import java.util.concurrent.ExecutionException +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean + +object MediaManager { + private const val TAG = "MediaManager" + private var attachedBrowserRef = WeakReference(null) + val justStarted: AtomicBoolean = AtomicBoolean(false) + + private val backgroundExecutor: ExecutorService = Executors.newSingleThreadExecutor() + + @JvmStatic + fun registerPlaybackObserver( + browserFuture: ListenableFuture?, + playbackViewModel: PlaybackViewModel + ) { + browserFuture?.onComplete( + onSuccess = { browser -> + val current = attachedBrowserRef.get() + if (current != browser) { + browser.addListener(object : Player.Listener { + override fun onEvents(player: Player, events: Player.Events) { + if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION) + || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED) + || events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) + ) { + val mediaId = player.currentMediaItem?.mediaId + val playing = player.playbackState == Player.STATE_READY + && player.playWhenReady + + playbackViewModel.update(mediaId, playing) + } + } + }) + + val mediaId = browser.getCurrentMediaItem()?.mediaId + val playing = + browser.getPlaybackState() == Player.STATE_READY && browser.getPlayWhenReady() + playbackViewModel.update(mediaId, playing) + + attachedBrowserRef = WeakReference(browser) + } else { + val mediaId = browser.getCurrentMediaItem()?.mediaId + val playing = + browser.getPlaybackState() == Player.STATE_READY && browser.getPlayWhenReady() + playbackViewModel.update(mediaId, playing) + } + }, + onFailure = { + Log.e(TAG, "Failed to get MediaBrowser instance", it) + } + ) + } + + @JvmStatic + fun onBrowserReleased(released: MediaBrowser?) { + val attached = attachedBrowserRef.get() + if (attached == released) { + attachedBrowserRef.clear() + } + } + + @JvmStatic + fun reset(mediaBrowserListenableFuture: ListenableFuture?) { + mediaBrowserListenableFuture?.onComplete { mediaBrowser -> + if (mediaBrowser.isPlaying()) { + mediaBrowser.pause() + } + + mediaBrowser.stop() + mediaBrowser.clearMediaItems() + clearDatabase() + } + } + + @JvmStatic + fun hide(mediaBrowserListenableFuture: ListenableFuture?) { + mediaBrowserListenableFuture?.onComplete { mediaBrowser -> + if (mediaBrowser.isPlaying()) { + mediaBrowser.pause() + } + } + } + + @JvmStatic + fun check(mediaBrowserListenableFuture: ListenableFuture?) { + mediaBrowserListenableFuture?.onComplete { mediaBrowser -> + if (mediaBrowser.mediaItemCount < 1) { + val media: MutableList? = queueRepository.getMedia() + if (!media.isNullOrEmpty()) { + init(mediaBrowserListenableFuture, media) + } + } + } + } + + private fun init( + mediaBrowserListenableFuture: ListenableFuture?, + media: MutableList + ) { + mediaBrowserListenableFuture?.onComplete { mediaBrowser -> + mediaBrowser.clearMediaItems() + mediaBrowser.setMediaItems(MappingUtil.mapMediaItems(media)) + mediaBrowser.seekTo( + queueRepository.getLastPlayedMediaIndex(), + queueRepository.getLastPlayedMediaTimestamp() + ) + mediaBrowser.prepare() + } + } + + @JvmStatic + fun startQueue( + mediaBrowserListenableFuture: ListenableFuture?, + media: MutableList, + startIndex: Int + ) { + mediaBrowserListenableFuture?.onComplete { browser -> + val items = MappingUtil.mapMediaItems(media) + + Handler(Looper.getMainLooper()).post { + justStarted.set(true) + browser.setMediaItems(items, startIndex, 0) + browser.prepare() + + val timelineListener = object : Player.Listener { + override fun onTimelineChanged(timeline: Timeline, reason: Int) { + val itemCount = browser.mediaItemCount + if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) { + browser.seekTo(startIndex, 0) + browser.play() + browser.removeListener(this) + } else { + Log.d( + TAG, + "Cannot start playback: itemCount=$itemCount, startIndex=$startIndex" + ) + } + } + } + browser.addListener(timelineListener) + } + + backgroundExecutor.execute { + Log.d(TAG, "Background: enqueuing to database") + enqueueDatabase(media, true, 0) + } + } + } + + @JvmStatic + fun startQueue(mediaBrowserListenableFuture: ListenableFuture?, media: Child?) { + mediaBrowserListenableFuture?.onComplete { browser -> + justStarted.set(true) + browser.setMediaItem(MappingUtil.mapMediaItem(media)) + browser.prepare() + browser.play() + enqueueDatabase(media, true, 0) + } + } + + @JvmStatic + fun playDownloadedMediaItem( + mediaBrowserListenableFuture: ListenableFuture?, + mediaItem: MediaItem? + ) { + if (mediaItem == null) + return + + mediaBrowserListenableFuture?.onComplete { mediaBrowser -> + justStarted.set(true) + mediaBrowser.setMediaItem(mediaItem) + mediaBrowser.prepare() + mediaBrowser.play() + clearDatabase() + } + } + + @JvmStatic + fun startRadio( + mediaBrowserListenableFuture: ListenableFuture?, + internetRadioStation: InternetRadioStation + ) { + mediaBrowserListenableFuture?.onComplete { browser -> + justStarted.set(true) + browser.setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation)) + browser.prepare() + browser.play() + } + } + + @JvmStatic + fun startPodcast( + mediaBrowserListenableFuture: ListenableFuture?, + podcastEpisode: PodcastEpisode? + ) { + mediaBrowserListenableFuture?.onComplete { browser -> + justStarted.set(true) + browser.setMediaItem(MappingUtil.mapMediaItem(podcastEpisode)) + browser.prepare() + browser.play() + } + } + + @JvmStatic + fun enqueue( + mediaBrowserListenableFuture: ListenableFuture?, + media: MutableList, + playImmediatelyAfter: Boolean + ) { + mediaBrowserListenableFuture?.onComplete { browser -> + Log.e(TAG, "enqueue") + if (playImmediatelyAfter && browser.getNextMediaItemIndex() != -1) { + enqueueDatabase(media, false, browser.getNextMediaItemIndex()) + browser.addMediaItems( + browser.getNextMediaItemIndex(), + MappingUtil.mapMediaItems(media) + ) + } else { + enqueueDatabase( + media, + false, + browser.mediaItemCount + ) + browser.addMediaItems(MappingUtil.mapMediaItems(media)) + } + } + } + + @JvmStatic + fun enqueue( + mediaBrowserListenableFuture: ListenableFuture?, + media: Child?, + playImmediatelyAfter: Boolean + ) { + mediaBrowserListenableFuture?.onComplete { browser -> + Log.e(TAG, "enqueue") + if (playImmediatelyAfter && browser.getNextMediaItemIndex() != -1) { + enqueueDatabase(media, false, browser.getNextMediaItemIndex()) + browser.addMediaItem( + browser.getNextMediaItemIndex(), + MappingUtil.mapMediaItem(media) + ) + } else { + enqueueDatabase( + media, + false, + browser.mediaItemCount + ) + browser.addMediaItem(MappingUtil.mapMediaItem(media)) + } + } + } + + @JvmStatic + fun shuffle( + mediaBrowserListenableFuture: ListenableFuture?, + media: MutableList, + startIndex: Int, + endIndex: Int + ) { + mediaBrowserListenableFuture?.onComplete { browser -> + Log.e(TAG, "shuffle") + browser.removeMediaItems(startIndex, endIndex + 1) + browser.addMediaItems( + MappingUtil.mapMediaItems(media).subList(startIndex, endIndex + 1) + ) + swapDatabase(media) + } + } + + @JvmStatic + fun swap( + mediaBrowserListenableFuture: ListenableFuture?, + media: MutableList?, + from: Int, + to: Int + ) { + mediaBrowserListenableFuture?.onComplete { mediaBrowser -> + Log.e(TAG, "swap") + mediaBrowser.moveMediaItem(from, to) + swapDatabase(media) + } + } + + @JvmStatic + fun remove( + mediaBrowserListenableFuture: ListenableFuture?, + media: MutableList, + toRemove: Int + ) { + mediaBrowserListenableFuture?.onComplete { mediaBrowser -> + Log.e(TAG, "remove") + if (mediaBrowser.mediaItemCount > 1 && + mediaBrowser.getCurrentMediaItemIndex() != toRemove + ) { + mediaBrowser.removeMediaItem(toRemove) + removeDatabase(media, toRemove) + } else { + removeDatabase(media, -1) + } + } + } + + @JvmStatic + fun removeRange( + mediaBrowserListenableFuture: ListenableFuture?, + media: MutableList, + fromItem: Int, + toItem: Int + ) { + mediaBrowserListenableFuture?.onComplete { mediaBrowser -> + Log.e(TAG, "remove range") + mediaBrowser.removeMediaItems(fromItem, toItem) + removeRangeDatabase(media, fromItem, toItem) + } + } + + @JvmStatic + fun getCurrentIndex( + mediaBrowserListenableFuture: ListenableFuture?, + callback: MediaIndexCallback + ) { + mediaBrowserListenableFuture?.onComplete { mediaBrowser -> + callback.onRecovery( + mediaBrowser.getCurrentMediaItemIndex() + ) + } + } + + @JvmStatic + fun setLastPlayedTimestamp(mediaItem: MediaItem?) { + if (mediaItem != null) queueRepository.setLastPlayedTimestamp(mediaItem.mediaId) + } + + @JvmStatic + fun setPlayingPausedTimestamp(mediaItem: MediaItem?, ms: Long) { + if (mediaItem != null) queueRepository.setPlayingPausedTimestamp(mediaItem.mediaId, ms) + } + + @JvmStatic + fun scrobble(mediaItem: MediaItem?, submission: Boolean) { + val mediaItemId = mediaItem?.mediaMetadata?.extras?.getString("id") ?: return + if (isScrobblingEnabled()) { + songRepository.scrobble(mediaItemId, submission) + } + } + + @JvmStatic + fun continuousPlay( + mediaItem: MediaItem?, + existingBrowserFuture: ListenableFuture? + ) { + if (mediaItem == null || !isContinuousPlayEnabled() || !isInstantMixUsable()) { + return + } + + setLastInstantMix() + + val instantMix = songRepository.getContinuousMix(mediaItem.mediaId, 25) + + instantMix.observeForever(object : Observer?> { + override fun onChanged(value: MutableList?) { + if (value.isNullOrEmpty()) { + return + } + + if (existingBrowserFuture != null) { + Log.d(TAG, "Continuous play: adding " + value.size + " tracks") + enqueue(existingBrowserFuture, value, true) + } + instantMix.removeObserver(this) + } + }) + } + + @JvmStatic + fun saveChronology(mediaItem: MediaItem?) { + if (mediaItem != null) { + chronologyRepository.insert(Chronology(mediaItem)) + } + } + + private val queueRepository: QueueRepository + get() = QueueRepository() + + private val songRepository: SongRepository + get() = SongRepository() + + private val chronologyRepository: ChronologyRepository + get() = ChronologyRepository() + + private fun enqueueDatabase(media: MutableList?, reset: Boolean, afterIndex: Int) { + queueRepository.insertAll(media, reset, afterIndex) + } + + private fun enqueueDatabase(media: Child?, reset: Boolean, afterIndex: Int) { + queueRepository.insert(media, reset, afterIndex) + } + + private fun swapDatabase(media: MutableList?) { + queueRepository.insertAll(media, true, 0) + } + + private fun removeDatabase(media: MutableList, toRemove: Int) { + if (toRemove != -1) { + media.removeAt(toRemove) + queueRepository.insertAll(media, true, 0) + } + } + + private fun removeRangeDatabase(media: MutableList, fromItem: Int, toItem: Int) { + val toRemove = media.subList(fromItem, toItem) + + media.removeAll(toRemove) + + queueRepository.insertAll(media, true, 0) + } + + private fun clearDatabase() { + queueRepository.deleteAll() + } + + private fun ListenableFuture?.onComplete(callback: (MediaBrowser) -> Unit) { + onComplete( + onSuccess = callback, + onFailure = { it.printStackTrace() } + ) + } + + private fun ListenableFuture?.onComplete( + onSuccess: (MediaBrowser) -> Unit, + onFailure: ((Exception) -> Unit), + ) { + this?.addListener({ + try { + if (isDone) { + onSuccess(get()) + } + } catch (e: ExecutionException) { + onFailure(e) + } catch (e: InterruptedException) { + onFailure(e) + } + }, MoreExecutors.directExecutor()) + } +}