diff --git a/app/src/main/java/com/dkanada/gramophone/database/QueueSongDao.java b/app/src/main/java/com/dkanada/gramophone/database/QueueSongDao.java index 69796902..9cf9ec9d 100644 --- a/app/src/main/java/com/dkanada/gramophone/database/QueueSongDao.java +++ b/app/src/main/java/com/dkanada/gramophone/database/QueueSongDao.java @@ -45,4 +45,15 @@ public abstract class QueueSongDao { insertQueueSongs(queueSongs); } + + @Transaction + public void updateQueues(List playingQueue, List shuffledQueue) { + // copy queues by value to avoid concurrent modification exceptions from database + App.getDatabase().songDao().deleteSongs(); + App.getDatabase().songDao().insertSongs(new ArrayList<>(playingQueue)); + + deleteQueueSongs(); + setQueue(new ArrayList<>(playingQueue), 0); + setQueue(new ArrayList<>(shuffledQueue), 1); + } } diff --git a/app/src/main/java/com/dkanada/gramophone/helper/MusicPlayerRemote.java b/app/src/main/java/com/dkanada/gramophone/helper/MusicPlayerRemote.java index 61838fb4..f4d4d5f1 100644 --- a/app/src/main/java/com/dkanada/gramophone/helper/MusicPlayerRemote.java +++ b/app/src/main/java/com/dkanada/gramophone/helper/MusicPlayerRemote.java @@ -111,19 +111,19 @@ public class MusicPlayerRemote { public static void playNextSong() { if (musicService != null) { - musicService.playNextSong(true); + musicService.playNextSong(); } } public static void playPreviousSong() { if (musicService != null) { - musicService.playPreviousSong(true); + musicService.playPreviousSong(); } } public static void back() { if (musicService != null) { - musicService.back(true); + musicService.back(); } } @@ -143,10 +143,10 @@ public class MusicPlayerRemote { public static void openQueue(final List queue, final int startPosition, final boolean startPlaying) { if (!tryToHandleOpenPlayingQueue(queue, startPosition) && musicService != null) { - musicService.openQueue(queue, startPosition, startPlaying); - if (!PreferenceUtil.getInstance(musicService).getRememberShuffle()){ + if (!PreferenceUtil.getInstance(musicService).getRememberShuffle()) { setShuffleMode(QueueManager.SHUFFLE_MODE_NONE); } + musicService.openQueue(queue, startPosition, startPlaying); } } @@ -157,8 +157,8 @@ public class MusicPlayerRemote { } if (!tryToHandleOpenPlayingQueue(queue, startPosition) && musicService != null) { - openQueue(queue, startPosition, startPlaying); setShuffleMode(QueueManager.SHUFFLE_MODE_SHUFFLE); + openQueue(queue, startPosition, startPlaying); } } diff --git a/app/src/main/java/com/dkanada/gramophone/helper/ShuffleHelper.java b/app/src/main/java/com/dkanada/gramophone/helper/ShuffleHelper.java deleted file mode 100644 index 5dbc148f..00000000 --- a/app/src/main/java/com/dkanada/gramophone/helper/ShuffleHelper.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.dkanada.gramophone.helper; - -import androidx.annotation.NonNull; - -import com.dkanada.gramophone.model.Song; - -import java.util.Collections; -import java.util.List; - -public class ShuffleHelper { - public static void makeShuffleList(@NonNull List listToShuffle, final int current) { - if (listToShuffle.isEmpty()) return; - - if (current >= 0) { - Song song = listToShuffle.remove(current); - - Collections.shuffle(listToShuffle); - listToShuffle.add(0, song); - } else { - Collections.shuffle(listToShuffle); - } - } -} diff --git a/app/src/main/java/com/dkanada/gramophone/service/MusicService.java b/app/src/main/java/com/dkanada/gramophone/service/MusicService.java index 80bfbb92..987db7f6 100644 --- a/app/src/main/java/com/dkanada/gramophone/service/MusicService.java +++ b/app/src/main/java/com/dkanada/gramophone/service/MusicService.java @@ -39,7 +39,6 @@ import com.dkanada.gramophone.BuildConfig; import com.dkanada.gramophone.R; import com.dkanada.gramophone.glide.BlurTransformation; import com.dkanada.gramophone.glide.CustomGlideRequest; -import com.dkanada.gramophone.helper.ShuffleHelper; import com.dkanada.gramophone.model.Playlist; import com.dkanada.gramophone.model.Song; import com.dkanada.gramophone.service.notifications.PlayingNotification; @@ -53,6 +52,7 @@ import com.dkanada.gramophone.util.Util; import com.dkanada.gramophone.views.widgets.AppWidgetAlbum; import com.dkanada.gramophone.views.widgets.AppWidgetCard; import com.dkanada.gramophone.views.widgets.AppWidgetClassic; +import com.google.android.exoplayer2.Player; import org.jellyfin.apiclient.interaction.EmptyResponse; import org.jellyfin.apiclient.interaction.Response; @@ -103,9 +103,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP public static final int TRACK_CHANGED = 1; public static final int TRACK_ENDED = 2; - public static final int PLAY_SONG = 3; - public static final int PREPARE_NEXT = 4; - public static final int SAVE_QUEUE = 0; public static final int LOAD_QUEUE = 9; @@ -127,27 +124,26 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP private MediaSessionCompat mediaSession; private PowerManager.WakeLock wakeLock; - private PlaybackHandler playerHandler; private Handler uiThreadHandler; private ThrottledSeekHandler throttledSeekHandler; private QueueHandler queueHandler; private ProgressHandler progressHandler; - private HandlerThread playerHandlerThread; private HandlerThread progressHandlerThread; private HandlerThread queueHandlerThread; public final QueueManager.QueueCallbacks queueCallbacks = new QueueManager.QueueCallbacks() { @Override public void onQueueChanged() { + playback.setQueue(queueManager.getPlayingQueue(), queueManager.getPosition(), queueManager.getRestoredProgress(), queueManager.isResetCurrentSong()); + saveState(); notifyChange(QUEUE_CHANGED); } @Override public void onRepeatModeChanged() { + playback.setRepeatMode(queueManager.getRepeatMode()); notifyChange(REPEAT_MODE_CHANGED); - // FIXME This call will be removed in a subsequent PR - prepareNext(); } @Override @@ -160,6 +156,11 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP @Override public void onStateChanged(int state) { notifyChange(STATE_CHANGED); + + if (state == Player.STATE_ENDED) { + playingNotification.stop(); + releaseWakeLock(); + } } @Override @@ -168,7 +169,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP if (ready) { progressHandler.sendEmptyMessage(TRACK_STARTED); - prepareNext(); } else if (reason == PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM) { progressHandler.sendEmptyMessage(TRACK_ENDED); } @@ -179,11 +179,10 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP acquireWakeLock(30000); if (reason == MEDIA_ITEM_TRANSITION_REASON_AUTO) { - playerHandler.sendEmptyMessage(TRACK_CHANGED); progressHandler.sendEmptyMessage(TRACK_CHANGED); + queueManager.setNextPosition(); } else if (reason == MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) { progressHandler.sendEmptyMessage(TRACK_CHANGED); - prepareNext(); } notifyChange(STATE_CHANGED); @@ -241,10 +240,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP queueManager = new QueueManager(this, queueCallbacks); - playerHandlerThread = new HandlerThread(PlaybackHandler.class.getName()); - playerHandlerThread.start(); - playerHandler = new PlaybackHandler(this, playerHandlerThread.getLooper()); - progressHandlerThread = new HandlerThread(ProgressHandler.class.getName()); progressHandlerThread.start(); progressHandler = new ProgressHandler(this, progressHandlerThread.getLooper()); @@ -290,12 +285,12 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP @Override public void onSkipToNext() { - playNextSong(true); + playNextSong(); } @Override public void onSkipToPrevious() { - back(true); + back(); } @Override @@ -357,10 +352,10 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } break; case ACTION_REWIND: - back(true); + back(); break; case ACTION_SKIP: - playNextSong(true); + playNextSong(); break; case ACTION_STOP: case ACTION_QUIT: @@ -422,42 +417,17 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP queueHandler.removeMessages(SAVE_QUEUE); queueHandler.sendEmptyMessage(SAVE_QUEUE); - PreferenceUtil.getInstance(this).setPosition(queueManager.position); PreferenceUtil.getInstance(this).setProgress(getSongProgressMillis()); } private void restoreState() { - queueManager.shuffleMode = PreferenceUtil.getInstance(this).getShuffle(); - queueManager.repeatMode = PreferenceUtil.getInstance(this).getRepeat(); - - notifyChange(SHUFFLE_MODE_CHANGED); - notifyChange(REPEAT_MODE_CHANGED); - queueHandler.removeMessages(LOAD_QUEUE); queueHandler.sendEmptyMessage(LOAD_QUEUE); } - // FIXME This will be refactored and partly moved to QueueManager in a subsequent PR private synchronized void restoreQueuesAndPositionIfNecessary() { if (!queuesRestored && queueManager.getPlayingQueue().isEmpty()) { - List restoredQueue = App.getDatabase().queueSongDao().getQueue(0); - List restoredOriginalQueue = App.getDatabase().queueSongDao().getQueue(1); - - int restoredPosition = PreferenceUtil.getInstance(this).getPosition(); - int restoredProgress = PreferenceUtil.getInstance(this).getProgress(); - - if (restoredQueue.size() > 0 && restoredQueue.size() == restoredOriginalQueue.size() && restoredPosition != -1) { - queueManager.originalPlayingQueue = restoredOriginalQueue; - queueManager.playingQueue = restoredQueue; - - queueManager.position = restoredPosition; - openCurrent(); - - if (restoredProgress > 0) seek(restoredProgress); - - handleChangeInternal(META_CHANGED); - handleChangeInternal(QUEUE_CHANGED); - } + queueManager.restoreQueue(); } queuesRestored = true; @@ -471,9 +441,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } private void releaseResources() { - playerHandler.removeCallbacksAndMessages(null); - playerHandlerThread.quitSafely(); - progressHandler.removeCallbacksAndMessages(null); progressHandlerThread.quitSafely(); @@ -492,34 +459,14 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP return playback != null && playback.isLoading(); } - - public void playNextSong(boolean force) { - playSongAt(queueManager.getNextPosition(force)); + public void playPreviousSong() { + queueManager.setPreviousPosition(); + playback.previous(); } - private synchronized void openTrackAndPrepareNextAt(int position) { - queueManager.position = position; - - openCurrent(); - playback.start(); - } - - private synchronized void openCurrent() { - if (queueManager.getCurrentSong() == null) return; - - playback.setDataSource(queueManager.getCurrentSong()); - } - - private void prepareNext() { - playerHandler.removeMessages(PREPARE_NEXT); - playerHandler.obtainMessage(PREPARE_NEXT).sendToTarget(); - } - - private synchronized void prepareNextImpl() { - if (queueManager.getCurrentSong() == null) return; - - queueManager.nextPosition = queueManager.getNextPosition(false); - playback.queueDataSource(queueManager.getSongAt(queueManager.nextPosition)); + public void playNextSong() { + queueManager.setNextPosition(); + playback.next(); } public void initNotification() { @@ -561,7 +508,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.albumName) .putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration) - .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, queueManager.position + 1) + .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, queueManager.getPosition() + 1) .putLong(MediaMetadataCompat.METADATA_KEY_YEAR, song.year) .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, null); @@ -615,29 +562,17 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP uiThreadHandler.post(runnable); } - // FIXME This will be refactored and partly moved to QueueManager in a subsequent PR public void openQueue(@Nullable final List playingQueue, final int startPosition, final boolean startPlaying) { if (playingQueue != null && !playingQueue.isEmpty() && startPosition >= 0 && startPosition < playingQueue.size()) { - // it is important to copy the playing queue here first as we might add or remove songs later - queueManager.originalPlayingQueue = new ArrayList<>(playingQueue); - queueManager.playingQueue = new ArrayList<>(queueManager.originalPlayingQueue); - - int position = startPosition; - if (queueManager.shuffleMode == QueueManager.SHUFFLE_MODE_SHUFFLE) { - ShuffleHelper.makeShuffleList(queueManager.playingQueue, startPosition); - position = 0; - } - - playSongAt(position); - - notifyChange(QUEUE_CHANGED); + queueManager.setPlayingQueueAndPosition(playingQueue, startPosition); + if (startPlaying) play(); } } public void playSongAt(final int position) { - // handle this on the handlers thread to avoid blocking the ui thread - playerHandler.removeMessages(PLAY_SONG); - playerHandler.obtainMessage(PLAY_SONG, position, 0).sendToTarget(); + queueManager.setPosition(position); + playback.playSongAt(position); + play(); } public void pause() { @@ -646,25 +581,17 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } } - public synchronized void play() { + public void play() { if (!playback.isPlaying()) { - if (!playback.isReady()) { - playSongAt(queueManager.position); - } else { - playback.start(); - } + playback.start(); } } - public void playPreviousSong(boolean force) { - playSongAt(queueManager.getPreviousPosition(force)); - } - - public void back(boolean force) { + public void back() { if (getSongProgressMillis() > 5000) { seek(0); } else { - playPreviousSong(force); + playPreviousSong(); } } @@ -709,19 +636,11 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP updateNotification(); updateMediaSessionMetadata(); updateMediaSessionState(); - PreferenceUtil.getInstance(this).setPosition(queueManager.position); + PreferenceUtil.getInstance(this).setPosition(queueManager.getPosition()); PreferenceUtil.getInstance(this).setProgress(getSongProgressMillis()); break; case QUEUE_CHANGED: - // because playing queue size might have changed updateMediaSessionMetadata(); - saveState(); - if (queueManager.getPlayingQueue().size() > 0) { - prepareNext(); - } else { - playback.pause(); - playingNotification.stop(); - } break; } } @@ -757,63 +676,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } } - private static final class PlaybackHandler extends Handler { - private final WeakReference mService; - - public PlaybackHandler(final MusicService service, @NonNull final Looper looper) { - super(looper); - mService = new WeakReference<>(service); - } - - @Override - public void handleMessage(@NonNull final Message msg) { - final MusicService service = mService.get(); - if (service == null) { - return; - } - - switch (msg.what) { - case TRACK_CHANGED: - if (service.queueManager.getRepeatMode() == QueueManager.REPEAT_MODE_NONE && service.queueManager.isLastTrack()) { - service.pause(); - service.seek(0); - } else { - service.queueManager.position = service.queueManager.nextPosition; - service.prepareNextImpl(); - service.notifyChange(QUEUE_CHANGED); - } - break; - - case TRACK_ENDED: - // FIXME This isn't used anywhere. This means releaseWakeLock() is never called - - // if there is a timer finished, don't continue - if (service.pendingQuit || service.queueManager.getRepeatMode() == QueueManager.REPEAT_MODE_NONE && service.queueManager.isLastTrack()) { - service.notifyChange(STATE_CHANGED); - service.seek(0); - if (service.pendingQuit) { - service.pendingQuit = false; - service.quit(); - break; - } - } else { - service.playNextSong(false); - } - - service.releaseWakeLock(); - break; - - case PLAY_SONG: - service.openTrackAndPrepareNextAt(msg.arg1); - break; - - case PREPARE_NEXT: - service.prepareNextImpl(); - break; - } - } - } - public class MusicBinder extends Binder { @NonNull public MusicService getService() { diff --git a/app/src/main/java/com/dkanada/gramophone/service/QueueManager.java b/app/src/main/java/com/dkanada/gramophone/service/QueueManager.java index 46d6cd4a..ed783563 100644 --- a/app/src/main/java/com/dkanada/gramophone/service/QueueManager.java +++ b/app/src/main/java/com/dkanada/gramophone/service/QueueManager.java @@ -3,18 +3,19 @@ package com.dkanada.gramophone.service; import android.content.Context; import com.dkanada.gramophone.App; -import com.dkanada.gramophone.helper.MusicPlayerRemote; -import com.dkanada.gramophone.helper.ShuffleHelper; import com.dkanada.gramophone.model.Song; import com.dkanada.gramophone.util.PreferenceUtil; +import com.google.android.exoplayer2.Player; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Optional; public class QueueManager { public static final int REPEAT_MODE_NONE = 0; - public static final int REPEAT_MODE_ALL = 1; - public static final int REPEAT_MODE_THIS = 2; + public static final int REPEAT_MODE_THIS = 1; + public static final int REPEAT_MODE_ALL = 2; public static final int SHUFFLE_MODE_NONE = 0; public static final int SHUFFLE_MODE_SHUFFLE = 1; @@ -22,20 +23,34 @@ public class QueueManager { private final Context context; private final QueueCallbacks callbacks; - List playingQueue = new ArrayList<>(); - List originalPlayingQueue = new ArrayList<>(); + private List playingQueue = new ArrayList<>(); + private List shuffledQueue = new ArrayList<>(); - int position = -1; - int nextPosition = -1; + private int position = 0; + private int restoredProgress = 0; + private boolean resetCurrentSong = true; - int shuffleMode; - int repeatMode; + private int shuffleMode; + private @Player.RepeatMode int repeatMode; public QueueManager(Context context, QueueCallbacks callbacks) { this.context = context; this.callbacks = callbacks; } + public void setPlayingQueueAndPosition(List queue, int position) { + this.position = position; + this.playingQueue = new ArrayList<>(queue); + this.shuffledQueue = new ArrayList<>(queue); + shuffleQueue(); + + callbacks.onQueueChanged(); + } + + public List getPlayingQueue() { + return shuffleMode == SHUFFLE_MODE_SHUFFLE ? shuffledQueue : playingQueue; + } + public int getPosition() { return position; } @@ -52,68 +67,32 @@ public class QueueManager { return null; } - public int getNextPosition(boolean force) { - int position = getPosition() + 1; + public void setPosition(int position) { + this.position = position; + } + + public void setNextPosition() { switch (getRepeatMode()) { - case REPEAT_MODE_ALL: - if (isLastTrack()) { - position = 0; - } - break; - case REPEAT_MODE_THIS: - if (force) { - if (isLastTrack()) { - position = 0; - } - } else { - position -= 1; - } - break; - default: case REPEAT_MODE_NONE: - if (isLastTrack()) { - position -= 1; - } + case REPEAT_MODE_THIS: + position = Math.min(position + 1, playingQueue.size() - 1); + break; + case REPEAT_MODE_ALL: + position = (position + 1) % playingQueue.size(); break; } - - return position; } - public int getPreviousPosition(boolean force) { - int newPosition = getPosition() - 1; - switch (repeatMode) { - case REPEAT_MODE_ALL: - if (newPosition < 0) { - newPosition = getPlayingQueue().size() - 1; - } - break; - case REPEAT_MODE_THIS: - if (force) { - if (newPosition < 0) { - newPosition = getPlayingQueue().size() - 1; - } - } else { - newPosition = getPosition(); - } - break; - default: + public void setPreviousPosition() { + switch (getRepeatMode()) { case REPEAT_MODE_NONE: - if (newPosition < 0) { - newPosition = 0; - } + case REPEAT_MODE_THIS: + position = Math.max(position - 1, 0); + break; + case REPEAT_MODE_ALL: + position = (position - 1 + playingQueue.size()) % playingQueue.size(); break; } - - return newPosition; - } - - public boolean isLastTrack() { - return getPosition() == getPlayingQueue().size() - 1; - } - - public List getPlayingQueue() { - return playingQueue; } public int getRepeatMode() { @@ -150,87 +129,110 @@ public class QueueManager { switch (shuffleMode) { case SHUFFLE_MODE_SHUFFLE: this.shuffleMode = shuffleMode; - ShuffleHelper.makeShuffleList(this.getPlayingQueue(), getPosition()); - position = 0; + shuffleQueue(); + break; case SHUFFLE_MODE_NONE: - this.shuffleMode = shuffleMode; String currentSongId = getCurrentSong().id; - playingQueue = new ArrayList<>(originalPlayingQueue); int newPosition = 0; - for (Song song : getPlayingQueue()) { - if (song.id == currentSongId) { - newPosition = getPlayingQueue().indexOf(song); - } + + Optional currentSong = playingQueue.stream() + .filter(song -> song.id.equals(currentSongId)) + .findFirst(); + + if (currentSong.isPresent()) { + newPosition = playingQueue.indexOf(currentSong.get()); } + shuffledQueue = new ArrayList<>(playingQueue); + position = newPosition; + this.shuffleMode = shuffleMode; break; } + resetCurrentSong = false; callbacks.onShuffleModeChanged(); callbacks.onQueueChanged(); } + private void shuffleQueue() { + if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { + this.shuffledQueue = new ArrayList<>(playingQueue); + + if (!shuffledQueue.isEmpty()) { + if (getPosition() >= 0) { + Song song = shuffledQueue.remove(getPosition()); + + Collections.shuffle(shuffledQueue); + shuffledQueue.add(0, song); + } else { + Collections.shuffle(shuffledQueue); + } + } + + position = 0; + } + } + public void addSong(int position, Song song) { playingQueue.add(position, song); - originalPlayingQueue.add(position, song); + shuffledQueue.add(position, song); + + resetCurrentSong = false; callbacks.onQueueChanged(); } public void addSong(Song song) { playingQueue.add(song); - originalPlayingQueue.add(song); + shuffledQueue.add(song); + + resetCurrentSong = false; callbacks.onQueueChanged(); } public void addSongs(int position, List songs) { playingQueue.addAll(position, songs); - originalPlayingQueue.addAll(position, songs); + shuffledQueue.addAll(position, songs); + + resetCurrentSong = false; callbacks.onQueueChanged(); } public void addSongs(List songs) { playingQueue.addAll(songs); - originalPlayingQueue.addAll(songs); + shuffledQueue.addAll(songs); + + resetCurrentSong = false; callbacks.onQueueChanged(); } public void removeSong(int position) { if (getShuffleMode() == SHUFFLE_MODE_NONE) { playingQueue.remove(position); - originalPlayingQueue.remove(position); + shuffledQueue.remove(position); } else { - originalPlayingQueue.remove(playingQueue.remove(position)); + playingQueue.remove(shuffledQueue.remove(position)); } - reposition(position); - callbacks.onQueueChanged(); - } - - // FIXME This will be refactored and removed in a subsequent PR - private void reposition(int deletedPosition) { int currentPosition = getPosition(); - if (deletedPosition < currentPosition) { - position = currentPosition - 1; - } else if (deletedPosition == currentPosition) { - if (playingQueue.size() > deletedPosition) { - MusicPlayerRemote.playSongAt(position); - } else { - MusicPlayerRemote.playSongAt(position - 1); - } + if (position != currentPosition) { + resetCurrentSong = false; } + + if (position < currentPosition) { + this.position = currentPosition - 1; + } + + callbacks.onQueueChanged(); } public void moveSong(int from, int to) { if (from == to) return; + final int currentPosition = getPosition(); - Song songToMove = playingQueue.remove(from); - playingQueue.add(to, songToMove); - if (getShuffleMode() == SHUFFLE_MODE_NONE) { - Song tmpSong = originalPlayingQueue.remove(from); - originalPlayingQueue.add(to, tmpSong); - } + Song songToMove = getPlayingQueue().remove(from); + getPlayingQueue().add(to, songToMove); if (from > currentPosition && to <= currentPosition) { position = currentPosition + 1; @@ -240,14 +242,15 @@ public class QueueManager { position = to; } + resetCurrentSong = false; callbacks.onQueueChanged(); } public void clearQueue() { playingQueue.clear(); - originalPlayingQueue.clear(); + shuffledQueue.clear(); - position = -1; + position = 0; callbacks.onQueueChanged(); } @@ -274,14 +277,38 @@ public class QueueManager { } } - public void saveQueue() { - // copy queues by value to avoid concurrent modification exceptions from database - App.getDatabase().songDao().deleteSongs(); - App.getDatabase().songDao().insertSongs(new ArrayList<>(playingQueue)); + public void restoreQueue() { + position = PreferenceUtil.getInstance(context).getPosition(); + restoredProgress = PreferenceUtil.getInstance(context).getProgress(); - App.getDatabase().queueSongDao().deleteQueueSongs(); - App.getDatabase().queueSongDao().setQueue(new ArrayList<>(playingQueue), 0); - App.getDatabase().queueSongDao().setQueue(new ArrayList<>(originalPlayingQueue), 1); + playingQueue = new ArrayList<>(App.getDatabase().queueSongDao().getQueue(0)); + shuffledQueue = new ArrayList<>(App.getDatabase().queueSongDao().getQueue(1)); + + shuffleMode = PreferenceUtil.getInstance(context).getShuffle(); + repeatMode = PreferenceUtil.getInstance(context).getRepeat(); + + callbacks.onQueueChanged(); + callbacks.onRepeatModeChanged(); + callbacks.onShuffleModeChanged(); + } + + public void saveQueue() { + PreferenceUtil.getInstance(context).setPosition(position); + App.getDatabase().queueSongDao().updateQueues(playingQueue, shuffledQueue); + } + + public int getRestoredProgress() { + int progress = restoredProgress; + restoredProgress = 0; + + return progress; + } + + public boolean isResetCurrentSong() { + boolean reset = resetCurrentSong; + resetCurrentSong = true; + + return reset; } interface QueueCallbacks { diff --git a/app/src/main/java/com/dkanada/gramophone/service/playback/LocalPlayer.java b/app/src/main/java/com/dkanada/gramophone/service/playback/LocalPlayer.java index 57595bba..20b093f7 100644 --- a/app/src/main/java/com/dkanada/gramophone/service/playback/LocalPlayer.java +++ b/app/src/main/java/com/dkanada/gramophone/service/playback/LocalPlayer.java @@ -30,6 +30,8 @@ import com.google.android.exoplayer2.util.MimeTypes; import java.io.File; import java.util.List; import java.util.Locale; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.stream.Collectors; public class LocalPlayer implements Playback { @@ -41,6 +43,8 @@ public class LocalPlayer implements Playback { private PlaybackCallbacks callbacks; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + @SuppressWarnings("FieldCanBeLocal") private final EventListener eventListener = new EventListener() { @Override @@ -64,14 +68,7 @@ public class LocalPlayer implements Playback { @Override public void onMediaItemTransition(MediaItem mediaItem, int reason) { Log.i(TAG, String.format("onMediaItemTransition: %s %d", mediaItem, reason)); - - if (exoPlayer.getMediaItemCount() > 1) { - exoPlayer.removeMediaItem(0); - } - - if (callbacks != null) { - callbacks.onTrackChanged(reason); - } + if (callbacks != null) callbacks.onTrackChanged(reason); } @Override @@ -115,66 +112,72 @@ public class LocalPlayer implements Playback { } @Override - public void setDataSource(Song song) { - MediaItem mediaItem = exoPlayer.getCurrentMediaItem(); + public void setQueue(List queue, int position, int progress, boolean resetCurrentSong) { + executorService.submit(() -> { + List mediaItems = createMediaItems(queue); - if (mediaItem != null && mediaItem.mediaId.equals(song.id)) { - return; - } + // TODO: Call this on main thread + if (resetCurrentSong) { + exoPlayer.setMediaItems(mediaItems, position, progress); + return; + } - exoPlayer.clearMediaItems(); - appendDataSource(song); - exoPlayer.seekTo(0, 0); + int currentPosition = exoPlayer.getCurrentWindowIndex(); + exoPlayer.removeMediaItems(0, currentPosition); + + if (exoPlayer.getMediaItemCount() > 1) { + exoPlayer.removeMediaItems(1, exoPlayer.getMediaItemCount()); + } + + if (position + 1 < mediaItems.size()) { + exoPlayer.addMediaItems(1, mediaItems.subList(position + 1, mediaItems.size())); + } + + exoPlayer.addMediaItems(0, mediaItems.subList(0, position)); + }); + } + + private List createMediaItems(List queue) { + return queue.stream().map(song -> { + File audio = new File(MusicUtil.getFileUri(song)); + Uri uri = Uri.fromFile(audio); + + if (!audio.exists()) { + uri = Uri.parse(MusicUtil.getTranscodeUri(song)); + } + + List containers = PreferenceUtil.getInstance(context).getDirectPlayCodecs().stream() + .map(codec -> codec.container.toLowerCase(Locale.ROOT)) + .collect(Collectors.toList()); + List codecs = PreferenceUtil.getInstance(context).getDirectPlayCodecs().stream() + .map(codec -> codec.codec.toLowerCase(Locale.ROOT)) + .collect(Collectors.toList()); + String maxBitrate = PreferenceUtil.getInstance(context).getMaximumBitrate(); + + MediaItem mediaItem; + + if (uri.toString().contains("file://") || (containers.contains(song.container.toLowerCase(Locale.ROOT)) && codecs.contains(song.codec.toLowerCase(Locale.ROOT)) && song.bitRate <= Integer.parseInt(maxBitrate))) { + mediaItem = new MediaItem.Builder() + .setUri(uri) + .setMediaId(song.id) + .build(); + } else { + mediaItem = new MediaItem.Builder() + .setUri(uri) + .setMediaId(song.id) + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build(); + } + + return mediaItem; + }).collect(Collectors.toList()); } @Override - public void queueDataSource(Song song) { - while (exoPlayer.getMediaItemCount() > 1) { - exoPlayer.removeMediaItem(1); + public void playSongAt(int position) { + if (exoPlayer.getMediaItemCount() > 0) { + exoPlayer.seekTo(Math.max(0, position) % exoPlayer.getMediaItemCount(), 0); } - - appendDataSource(song); - } - - private void appendDataSource(Song song) { - File audio = new File(MusicUtil.getFileUri(song)); - Uri uri = Uri.fromFile(audio); - - if (!audio.exists()) { - uri = Uri.parse(MusicUtil.getTranscodeUri(song)); - } - - List containers = PreferenceUtil.getInstance(context).getDirectPlayCodecs().stream() - .map(codec -> codec.container.toLowerCase(Locale.ROOT)) - .collect(Collectors.toList()); - - List codecs = PreferenceUtil.getInstance(context).getDirectPlayCodecs().stream() - .map(codec -> codec.codec.toLowerCase(Locale.ROOT)) - .collect(Collectors.toList()); - - String maxBitrate = PreferenceUtil.getInstance(context).getMaximumBitrate(); - - MediaItem mediaItem; - - boolean shouldDirectPlay = - containers.contains(song.container.toLowerCase(Locale.ROOT)) && - codecs.contains(song.codec.toLowerCase(Locale.ROOT)) && - song.bitRate <= Integer.parseInt(maxBitrate); - - if (uri.toString().contains("file://") || shouldDirectPlay || !song.supportsTranscoding) { - mediaItem = new MediaItem.Builder() - .setUri(uri) - .setMediaId(song.id) - .build(); - } else { - mediaItem = new MediaItem.Builder() - .setUri(uri) - .setMediaId(song.id) - .setMimeType(MimeTypes.APPLICATION_M3U8) - .build(); - } - - exoPlayer.addMediaItem(mediaItem); } private DataSource.Factory buildDataSourceFactory() { @@ -230,6 +233,21 @@ public class LocalPlayer implements Playback { exoPlayer.release(); } + @Override + public void previous() { + exoPlayer.previous(); + } + + @Override + public void next() { + exoPlayer.next(); + } + + @Override + public void setRepeatMode(@Player.RepeatMode int repeatMode) { + exoPlayer.setRepeatMode(repeatMode); + } + @Override public int getProgress() { return (int) exoPlayer.getCurrentPosition(); diff --git a/app/src/main/java/com/dkanada/gramophone/service/playback/Playback.java b/app/src/main/java/com/dkanada/gramophone/service/playback/Playback.java index dddc1089..6570e6e3 100644 --- a/app/src/main/java/com/dkanada/gramophone/service/playback/Playback.java +++ b/app/src/main/java/com/dkanada/gramophone/service/playback/Playback.java @@ -1,11 +1,14 @@ package com.dkanada.gramophone.service.playback; import com.dkanada.gramophone.model.Song; +import com.google.android.exoplayer2.Player; + +import java.util.List; public interface Playback { - void setDataSource(Song song); + void setQueue(List queue, int position, int progress, boolean resetCurrentSong); - void queueDataSource(Song song); + void playSongAt(int position); void setCallbacks(PlaybackCallbacks callbacks); @@ -21,6 +24,12 @@ public interface Playback { void stop(); + void previous(); + + void next(); + + void setRepeatMode(@Player.RepeatMode int repeatMode); + int getProgress(); int getDuration(); diff --git a/app/src/main/java/com/dkanada/gramophone/util/PreferenceUtil.java b/app/src/main/java/com/dkanada/gramophone/util/PreferenceUtil.java index d8e03152..13c851dc 100644 --- a/app/src/main/java/com/dkanada/gramophone/util/PreferenceUtil.java +++ b/app/src/main/java/com/dkanada/gramophone/util/PreferenceUtil.java @@ -170,7 +170,7 @@ public final class PreferenceUtil { } public int getPosition() { - return mPreferences.getInt(POSITION, -1); + return mPreferences.getInt(POSITION, 0); } public void setPosition(int position) { @@ -178,7 +178,7 @@ public final class PreferenceUtil { } public int getProgress() { - return mPreferences.getInt(PROGRESS, -1); + return mPreferences.getInt(PROGRESS, 0); } public void setProgress(int progress) {