Merge pull request #211 from jakobkukla/new-rewrite-playback

Cleanup and rework playback and shuffle logic
This commit is contained in:
dkanada 2022-06-19 12:43:00 +09:00 committed by GitHub
commit 4f7f315ccd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 269 additions and 365 deletions

View file

@ -45,4 +45,15 @@ public abstract class QueueSongDao {
insertQueueSongs(queueSongs); insertQueueSongs(queueSongs);
} }
@Transaction
public void updateQueues(List<Song> playingQueue, List<Song> 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);
}
} }

View file

@ -111,19 +111,19 @@ public class MusicPlayerRemote {
public static void playNextSong() { public static void playNextSong() {
if (musicService != null) { if (musicService != null) {
musicService.playNextSong(true); musicService.playNextSong();
} }
} }
public static void playPreviousSong() { public static void playPreviousSong() {
if (musicService != null) { if (musicService != null) {
musicService.playPreviousSong(true); musicService.playPreviousSong();
} }
} }
public static void back() { public static void back() {
if (musicService != null) { if (musicService != null) {
musicService.back(true); musicService.back();
} }
} }
@ -143,10 +143,10 @@ public class MusicPlayerRemote {
public static void openQueue(final List<Song> queue, final int startPosition, final boolean startPlaying) { public static void openQueue(final List<Song> queue, final int startPosition, final boolean startPlaying) {
if (!tryToHandleOpenPlayingQueue(queue, startPosition) && musicService != null) { 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); setShuffleMode(QueueManager.SHUFFLE_MODE_NONE);
} }
musicService.openQueue(queue, startPosition, startPlaying);
} }
} }
@ -157,8 +157,8 @@ public class MusicPlayerRemote {
} }
if (!tryToHandleOpenPlayingQueue(queue, startPosition) && musicService != null) { if (!tryToHandleOpenPlayingQueue(queue, startPosition) && musicService != null) {
openQueue(queue, startPosition, startPlaying);
setShuffleMode(QueueManager.SHUFFLE_MODE_SHUFFLE); setShuffleMode(QueueManager.SHUFFLE_MODE_SHUFFLE);
openQueue(queue, startPosition, startPlaying);
} }
} }

View file

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

View file

@ -39,7 +39,6 @@ import com.dkanada.gramophone.BuildConfig;
import com.dkanada.gramophone.R; import com.dkanada.gramophone.R;
import com.dkanada.gramophone.glide.BlurTransformation; import com.dkanada.gramophone.glide.BlurTransformation;
import com.dkanada.gramophone.glide.CustomGlideRequest; import com.dkanada.gramophone.glide.CustomGlideRequest;
import com.dkanada.gramophone.helper.ShuffleHelper;
import com.dkanada.gramophone.model.Playlist; import com.dkanada.gramophone.model.Playlist;
import com.dkanada.gramophone.model.Song; import com.dkanada.gramophone.model.Song;
import com.dkanada.gramophone.service.notifications.PlayingNotification; 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.AppWidgetAlbum;
import com.dkanada.gramophone.views.widgets.AppWidgetCard; import com.dkanada.gramophone.views.widgets.AppWidgetCard;
import com.dkanada.gramophone.views.widgets.AppWidgetClassic; import com.dkanada.gramophone.views.widgets.AppWidgetClassic;
import com.google.android.exoplayer2.Player;
import org.jellyfin.apiclient.interaction.EmptyResponse; import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.Response; 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_CHANGED = 1;
public static final int TRACK_ENDED = 2; 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 SAVE_QUEUE = 0;
public static final int LOAD_QUEUE = 9; public static final int LOAD_QUEUE = 9;
@ -127,27 +124,26 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
private MediaSessionCompat mediaSession; private MediaSessionCompat mediaSession;
private PowerManager.WakeLock wakeLock; private PowerManager.WakeLock wakeLock;
private PlaybackHandler playerHandler;
private Handler uiThreadHandler; private Handler uiThreadHandler;
private ThrottledSeekHandler throttledSeekHandler; private ThrottledSeekHandler throttledSeekHandler;
private QueueHandler queueHandler; private QueueHandler queueHandler;
private ProgressHandler progressHandler; private ProgressHandler progressHandler;
private HandlerThread playerHandlerThread;
private HandlerThread progressHandlerThread; private HandlerThread progressHandlerThread;
private HandlerThread queueHandlerThread; private HandlerThread queueHandlerThread;
public final QueueManager.QueueCallbacks queueCallbacks = new QueueManager.QueueCallbacks() { public final QueueManager.QueueCallbacks queueCallbacks = new QueueManager.QueueCallbacks() {
@Override @Override
public void onQueueChanged() { public void onQueueChanged() {
playback.setQueue(queueManager.getPlayingQueue(), queueManager.getPosition(), queueManager.getRestoredProgress(), queueManager.isResetCurrentSong());
saveState();
notifyChange(QUEUE_CHANGED); notifyChange(QUEUE_CHANGED);
} }
@Override @Override
public void onRepeatModeChanged() { public void onRepeatModeChanged() {
playback.setRepeatMode(queueManager.getRepeatMode());
notifyChange(REPEAT_MODE_CHANGED); notifyChange(REPEAT_MODE_CHANGED);
// FIXME This call will be removed in a subsequent PR
prepareNext();
} }
@Override @Override
@ -160,6 +156,11 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
@Override @Override
public void onStateChanged(int state) { public void onStateChanged(int state) {
notifyChange(STATE_CHANGED); notifyChange(STATE_CHANGED);
if (state == Player.STATE_ENDED) {
playingNotification.stop();
releaseWakeLock();
}
} }
@Override @Override
@ -168,7 +169,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
if (ready) { if (ready) {
progressHandler.sendEmptyMessage(TRACK_STARTED); progressHandler.sendEmptyMessage(TRACK_STARTED);
prepareNext();
} else if (reason == PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM) { } else if (reason == PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM) {
progressHandler.sendEmptyMessage(TRACK_ENDED); progressHandler.sendEmptyMessage(TRACK_ENDED);
} }
@ -179,11 +179,10 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
acquireWakeLock(30000); acquireWakeLock(30000);
if (reason == MEDIA_ITEM_TRANSITION_REASON_AUTO) { if (reason == MEDIA_ITEM_TRANSITION_REASON_AUTO) {
playerHandler.sendEmptyMessage(TRACK_CHANGED);
progressHandler.sendEmptyMessage(TRACK_CHANGED); progressHandler.sendEmptyMessage(TRACK_CHANGED);
queueManager.setNextPosition();
} else if (reason == MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) { } else if (reason == MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) {
progressHandler.sendEmptyMessage(TRACK_CHANGED); progressHandler.sendEmptyMessage(TRACK_CHANGED);
prepareNext();
} }
notifyChange(STATE_CHANGED); notifyChange(STATE_CHANGED);
@ -241,10 +240,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
queueManager = new QueueManager(this, queueCallbacks); 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 = new HandlerThread(ProgressHandler.class.getName());
progressHandlerThread.start(); progressHandlerThread.start();
progressHandler = new ProgressHandler(this, progressHandlerThread.getLooper()); progressHandler = new ProgressHandler(this, progressHandlerThread.getLooper());
@ -290,12 +285,12 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
@Override @Override
public void onSkipToNext() { public void onSkipToNext() {
playNextSong(true); playNextSong();
} }
@Override @Override
public void onSkipToPrevious() { public void onSkipToPrevious() {
back(true); back();
} }
@Override @Override
@ -357,10 +352,10 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
} }
break; break;
case ACTION_REWIND: case ACTION_REWIND:
back(true); back();
break; break;
case ACTION_SKIP: case ACTION_SKIP:
playNextSong(true); playNextSong();
break; break;
case ACTION_STOP: case ACTION_STOP:
case ACTION_QUIT: case ACTION_QUIT:
@ -422,42 +417,17 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
queueHandler.removeMessages(SAVE_QUEUE); queueHandler.removeMessages(SAVE_QUEUE);
queueHandler.sendEmptyMessage(SAVE_QUEUE); queueHandler.sendEmptyMessage(SAVE_QUEUE);
PreferenceUtil.getInstance(this).setPosition(queueManager.position);
PreferenceUtil.getInstance(this).setProgress(getSongProgressMillis()); PreferenceUtil.getInstance(this).setProgress(getSongProgressMillis());
} }
private void restoreState() { 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.removeMessages(LOAD_QUEUE);
queueHandler.sendEmptyMessage(LOAD_QUEUE); queueHandler.sendEmptyMessage(LOAD_QUEUE);
} }
// FIXME This will be refactored and partly moved to QueueManager in a subsequent PR
private synchronized void restoreQueuesAndPositionIfNecessary() { private synchronized void restoreQueuesAndPositionIfNecessary() {
if (!queuesRestored && queueManager.getPlayingQueue().isEmpty()) { if (!queuesRestored && queueManager.getPlayingQueue().isEmpty()) {
List<Song> restoredQueue = App.getDatabase().queueSongDao().getQueue(0); queueManager.restoreQueue();
List<Song> 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);
}
} }
queuesRestored = true; queuesRestored = true;
@ -471,9 +441,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
} }
private void releaseResources() { private void releaseResources() {
playerHandler.removeCallbacksAndMessages(null);
playerHandlerThread.quitSafely();
progressHandler.removeCallbacksAndMessages(null); progressHandler.removeCallbacksAndMessages(null);
progressHandlerThread.quitSafely(); progressHandlerThread.quitSafely();
@ -492,34 +459,14 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
return playback != null && playback.isLoading(); return playback != null && playback.isLoading();
} }
public void playPreviousSong() {
public void playNextSong(boolean force) { queueManager.setPreviousPosition();
playSongAt(queueManager.getNextPosition(force)); playback.previous();
} }
private synchronized void openTrackAndPrepareNextAt(int position) { public void playNextSong() {
queueManager.position = position; queueManager.setNextPosition();
playback.next();
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 initNotification() { 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_ALBUM, song.albumName)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title) .putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration) .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) .putLong(MediaMetadataCompat.METADATA_KEY_YEAR, song.year)
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, null); .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, null);
@ -615,29 +562,17 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
uiThreadHandler.post(runnable); uiThreadHandler.post(runnable);
} }
// FIXME This will be refactored and partly moved to QueueManager in a subsequent PR
public void openQueue(@Nullable final List<Song> playingQueue, final int startPosition, final boolean startPlaying) { public void openQueue(@Nullable final List<Song> playingQueue, final int startPosition, final boolean startPlaying) {
if (playingQueue != null && !playingQueue.isEmpty() && startPosition >= 0 && startPosition < playingQueue.size()) { 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.setPlayingQueueAndPosition(playingQueue, startPosition);
queueManager.originalPlayingQueue = new ArrayList<>(playingQueue); if (startPlaying) play();
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);
} }
} }
public void playSongAt(final int position) { public void playSongAt(final int position) {
// handle this on the handlers thread to avoid blocking the ui thread queueManager.setPosition(position);
playerHandler.removeMessages(PLAY_SONG); playback.playSongAt(position);
playerHandler.obtainMessage(PLAY_SONG, position, 0).sendToTarget(); play();
} }
public void pause() { 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.isPlaying()) {
if (!playback.isReady()) { playback.start();
playSongAt(queueManager.position);
} else {
playback.start();
}
} }
} }
public void playPreviousSong(boolean force) { public void back() {
playSongAt(queueManager.getPreviousPosition(force));
}
public void back(boolean force) {
if (getSongProgressMillis() > 5000) { if (getSongProgressMillis() > 5000) {
seek(0); seek(0);
} else { } else {
playPreviousSong(force); playPreviousSong();
} }
} }
@ -709,19 +636,11 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
updateNotification(); updateNotification();
updateMediaSessionMetadata(); updateMediaSessionMetadata();
updateMediaSessionState(); updateMediaSessionState();
PreferenceUtil.getInstance(this).setPosition(queueManager.position); PreferenceUtil.getInstance(this).setPosition(queueManager.getPosition());
PreferenceUtil.getInstance(this).setProgress(getSongProgressMillis()); PreferenceUtil.getInstance(this).setProgress(getSongProgressMillis());
break; break;
case QUEUE_CHANGED: case QUEUE_CHANGED:
// because playing queue size might have changed
updateMediaSessionMetadata(); updateMediaSessionMetadata();
saveState();
if (queueManager.getPlayingQueue().size() > 0) {
prepareNext();
} else {
playback.pause();
playingNotification.stop();
}
break; break;
} }
} }
@ -757,63 +676,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
} }
} }
private static final class PlaybackHandler extends Handler {
private final WeakReference<MusicService> 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 { public class MusicBinder extends Binder {
@NonNull @NonNull
public MusicService getService() { public MusicService getService() {

View file

@ -3,18 +3,19 @@ package com.dkanada.gramophone.service;
import android.content.Context; import android.content.Context;
import com.dkanada.gramophone.App; 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.model.Song;
import com.dkanada.gramophone.util.PreferenceUtil; import com.dkanada.gramophone.util.PreferenceUtil;
import com.google.android.exoplayer2.Player;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
public class QueueManager { public class QueueManager {
public static final int REPEAT_MODE_NONE = 0; public static final int REPEAT_MODE_NONE = 0;
public static final int REPEAT_MODE_ALL = 1; public static final int REPEAT_MODE_THIS = 1;
public static final int REPEAT_MODE_THIS = 2; public static final int REPEAT_MODE_ALL = 2;
public static final int SHUFFLE_MODE_NONE = 0; public static final int SHUFFLE_MODE_NONE = 0;
public static final int SHUFFLE_MODE_SHUFFLE = 1; public static final int SHUFFLE_MODE_SHUFFLE = 1;
@ -22,20 +23,34 @@ public class QueueManager {
private final Context context; private final Context context;
private final QueueCallbacks callbacks; private final QueueCallbacks callbacks;
List<Song> playingQueue = new ArrayList<>(); private List<Song> playingQueue = new ArrayList<>();
List<Song> originalPlayingQueue = new ArrayList<>(); private List<Song> shuffledQueue = new ArrayList<>();
int position = -1; private int position = 0;
int nextPosition = -1; private int restoredProgress = 0;
private boolean resetCurrentSong = true;
int shuffleMode; private int shuffleMode;
int repeatMode; private @Player.RepeatMode int repeatMode;
public QueueManager(Context context, QueueCallbacks callbacks) { public QueueManager(Context context, QueueCallbacks callbacks) {
this.context = context; this.context = context;
this.callbacks = callbacks; this.callbacks = callbacks;
} }
public void setPlayingQueueAndPosition(List<Song> queue, int position) {
this.position = position;
this.playingQueue = new ArrayList<>(queue);
this.shuffledQueue = new ArrayList<>(queue);
shuffleQueue();
callbacks.onQueueChanged();
}
public List<Song> getPlayingQueue() {
return shuffleMode == SHUFFLE_MODE_SHUFFLE ? shuffledQueue : playingQueue;
}
public int getPosition() { public int getPosition() {
return position; return position;
} }
@ -52,68 +67,32 @@ public class QueueManager {
return null; return null;
} }
public int getNextPosition(boolean force) { public void setPosition(int position) {
int position = getPosition() + 1; this.position = position;
}
public void setNextPosition() {
switch (getRepeatMode()) { 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: case REPEAT_MODE_NONE:
if (isLastTrack()) { case REPEAT_MODE_THIS:
position -= 1; position = Math.min(position + 1, playingQueue.size() - 1);
} break;
case REPEAT_MODE_ALL:
position = (position + 1) % playingQueue.size();
break; break;
} }
return position;
} }
public int getPreviousPosition(boolean force) { public void setPreviousPosition() {
int newPosition = getPosition() - 1; switch (getRepeatMode()) {
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:
case REPEAT_MODE_NONE: case REPEAT_MODE_NONE:
if (newPosition < 0) { case REPEAT_MODE_THIS:
newPosition = 0; position = Math.max(position - 1, 0);
} break;
case REPEAT_MODE_ALL:
position = (position - 1 + playingQueue.size()) % playingQueue.size();
break; break;
} }
return newPosition;
}
public boolean isLastTrack() {
return getPosition() == getPlayingQueue().size() - 1;
}
public List<Song> getPlayingQueue() {
return playingQueue;
} }
public int getRepeatMode() { public int getRepeatMode() {
@ -150,87 +129,110 @@ public class QueueManager {
switch (shuffleMode) { switch (shuffleMode) {
case SHUFFLE_MODE_SHUFFLE: case SHUFFLE_MODE_SHUFFLE:
this.shuffleMode = shuffleMode; this.shuffleMode = shuffleMode;
ShuffleHelper.makeShuffleList(this.getPlayingQueue(), getPosition()); shuffleQueue();
position = 0;
break; break;
case SHUFFLE_MODE_NONE: case SHUFFLE_MODE_NONE:
this.shuffleMode = shuffleMode;
String currentSongId = getCurrentSong().id; String currentSongId = getCurrentSong().id;
playingQueue = new ArrayList<>(originalPlayingQueue);
int newPosition = 0; int newPosition = 0;
for (Song song : getPlayingQueue()) {
if (song.id == currentSongId) { Optional<Song> currentSong = playingQueue.stream()
newPosition = getPlayingQueue().indexOf(song); .filter(song -> song.id.equals(currentSongId))
} .findFirst();
if (currentSong.isPresent()) {
newPosition = playingQueue.indexOf(currentSong.get());
} }
shuffledQueue = new ArrayList<>(playingQueue);
position = newPosition; position = newPosition;
this.shuffleMode = shuffleMode;
break; break;
} }
resetCurrentSong = false;
callbacks.onShuffleModeChanged(); callbacks.onShuffleModeChanged();
callbacks.onQueueChanged(); 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) { public void addSong(int position, Song song) {
playingQueue.add(position, song); playingQueue.add(position, song);
originalPlayingQueue.add(position, song); shuffledQueue.add(position, song);
resetCurrentSong = false;
callbacks.onQueueChanged(); callbacks.onQueueChanged();
} }
public void addSong(Song song) { public void addSong(Song song) {
playingQueue.add(song); playingQueue.add(song);
originalPlayingQueue.add(song); shuffledQueue.add(song);
resetCurrentSong = false;
callbacks.onQueueChanged(); callbacks.onQueueChanged();
} }
public void addSongs(int position, List<Song> songs) { public void addSongs(int position, List<Song> songs) {
playingQueue.addAll(position, songs); playingQueue.addAll(position, songs);
originalPlayingQueue.addAll(position, songs); shuffledQueue.addAll(position, songs);
resetCurrentSong = false;
callbacks.onQueueChanged(); callbacks.onQueueChanged();
} }
public void addSongs(List<Song> songs) { public void addSongs(List<Song> songs) {
playingQueue.addAll(songs); playingQueue.addAll(songs);
originalPlayingQueue.addAll(songs); shuffledQueue.addAll(songs);
resetCurrentSong = false;
callbacks.onQueueChanged(); callbacks.onQueueChanged();
} }
public void removeSong(int position) { public void removeSong(int position) {
if (getShuffleMode() == SHUFFLE_MODE_NONE) { if (getShuffleMode() == SHUFFLE_MODE_NONE) {
playingQueue.remove(position); playingQueue.remove(position);
originalPlayingQueue.remove(position); shuffledQueue.remove(position);
} else { } 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(); int currentPosition = getPosition();
if (deletedPosition < currentPosition) { if (position != currentPosition) {
position = currentPosition - 1; resetCurrentSong = false;
} else if (deletedPosition == currentPosition) {
if (playingQueue.size() > deletedPosition) {
MusicPlayerRemote.playSongAt(position);
} else {
MusicPlayerRemote.playSongAt(position - 1);
}
} }
if (position < currentPosition) {
this.position = currentPosition - 1;
}
callbacks.onQueueChanged();
} }
public void moveSong(int from, int to) { public void moveSong(int from, int to) {
if (from == to) return; if (from == to) return;
final int currentPosition = getPosition(); final int currentPosition = getPosition();
Song songToMove = playingQueue.remove(from); Song songToMove = getPlayingQueue().remove(from);
playingQueue.add(to, songToMove); getPlayingQueue().add(to, songToMove);
if (getShuffleMode() == SHUFFLE_MODE_NONE) {
Song tmpSong = originalPlayingQueue.remove(from);
originalPlayingQueue.add(to, tmpSong);
}
if (from > currentPosition && to <= currentPosition) { if (from > currentPosition && to <= currentPosition) {
position = currentPosition + 1; position = currentPosition + 1;
@ -240,14 +242,15 @@ public class QueueManager {
position = to; position = to;
} }
resetCurrentSong = false;
callbacks.onQueueChanged(); callbacks.onQueueChanged();
} }
public void clearQueue() { public void clearQueue() {
playingQueue.clear(); playingQueue.clear();
originalPlayingQueue.clear(); shuffledQueue.clear();
position = -1; position = 0;
callbacks.onQueueChanged(); callbacks.onQueueChanged();
} }
@ -274,14 +277,38 @@ public class QueueManager {
} }
} }
public void saveQueue() { public void restoreQueue() {
// copy queues by value to avoid concurrent modification exceptions from database position = PreferenceUtil.getInstance(context).getPosition();
App.getDatabase().songDao().deleteSongs(); restoredProgress = PreferenceUtil.getInstance(context).getProgress();
App.getDatabase().songDao().insertSongs(new ArrayList<>(playingQueue));
App.getDatabase().queueSongDao().deleteQueueSongs(); playingQueue = new ArrayList<>(App.getDatabase().queueSongDao().getQueue(0));
App.getDatabase().queueSongDao().setQueue(new ArrayList<>(playingQueue), 0); shuffledQueue = new ArrayList<>(App.getDatabase().queueSongDao().getQueue(1));
App.getDatabase().queueSongDao().setQueue(new ArrayList<>(originalPlayingQueue), 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 { interface QueueCallbacks {

View file

@ -30,6 +30,8 @@ import com.google.android.exoplayer2.util.MimeTypes;
import java.io.File; import java.io.File;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class LocalPlayer implements Playback { public class LocalPlayer implements Playback {
@ -41,6 +43,8 @@ public class LocalPlayer implements Playback {
private PlaybackCallbacks callbacks; private PlaybackCallbacks callbacks;
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
@SuppressWarnings("FieldCanBeLocal") @SuppressWarnings("FieldCanBeLocal")
private final EventListener eventListener = new EventListener() { private final EventListener eventListener = new EventListener() {
@Override @Override
@ -64,14 +68,7 @@ public class LocalPlayer implements Playback {
@Override @Override
public void onMediaItemTransition(MediaItem mediaItem, int reason) { public void onMediaItemTransition(MediaItem mediaItem, int reason) {
Log.i(TAG, String.format("onMediaItemTransition: %s %d", mediaItem, reason)); Log.i(TAG, String.format("onMediaItemTransition: %s %d", mediaItem, reason));
if (callbacks != null) callbacks.onTrackChanged(reason);
if (exoPlayer.getMediaItemCount() > 1) {
exoPlayer.removeMediaItem(0);
}
if (callbacks != null) {
callbacks.onTrackChanged(reason);
}
} }
@Override @Override
@ -115,66 +112,72 @@ public class LocalPlayer implements Playback {
} }
@Override @Override
public void setDataSource(Song song) { public void setQueue(List<Song> queue, int position, int progress, boolean resetCurrentSong) {
MediaItem mediaItem = exoPlayer.getCurrentMediaItem(); executorService.submit(() -> {
List<MediaItem> mediaItems = createMediaItems(queue);
if (mediaItem != null && mediaItem.mediaId.equals(song.id)) { // TODO: Call this on main thread
return; if (resetCurrentSong) {
} exoPlayer.setMediaItems(mediaItems, position, progress);
return;
}
exoPlayer.clearMediaItems(); int currentPosition = exoPlayer.getCurrentWindowIndex();
appendDataSource(song); exoPlayer.removeMediaItems(0, currentPosition);
exoPlayer.seekTo(0, 0);
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<MediaItem> createMediaItems(List<Song> 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<String> containers = PreferenceUtil.getInstance(context).getDirectPlayCodecs().stream()
.map(codec -> codec.container.toLowerCase(Locale.ROOT))
.collect(Collectors.toList());
List<String> 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 @Override
public void queueDataSource(Song song) { public void playSongAt(int position) {
while (exoPlayer.getMediaItemCount() > 1) { if (exoPlayer.getMediaItemCount() > 0) {
exoPlayer.removeMediaItem(1); 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<String> containers = PreferenceUtil.getInstance(context).getDirectPlayCodecs().stream()
.map(codec -> codec.container.toLowerCase(Locale.ROOT))
.collect(Collectors.toList());
List<String> 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() { private DataSource.Factory buildDataSourceFactory() {
@ -230,6 +233,21 @@ public class LocalPlayer implements Playback {
exoPlayer.release(); 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 @Override
public int getProgress() { public int getProgress() {
return (int) exoPlayer.getCurrentPosition(); return (int) exoPlayer.getCurrentPosition();

View file

@ -1,11 +1,14 @@
package com.dkanada.gramophone.service.playback; package com.dkanada.gramophone.service.playback;
import com.dkanada.gramophone.model.Song; import com.dkanada.gramophone.model.Song;
import com.google.android.exoplayer2.Player;
import java.util.List;
public interface Playback { public interface Playback {
void setDataSource(Song song); void setQueue(List<Song> queue, int position, int progress, boolean resetCurrentSong);
void queueDataSource(Song song); void playSongAt(int position);
void setCallbacks(PlaybackCallbacks callbacks); void setCallbacks(PlaybackCallbacks callbacks);
@ -21,6 +24,12 @@ public interface Playback {
void stop(); void stop();
void previous();
void next();
void setRepeatMode(@Player.RepeatMode int repeatMode);
int getProgress(); int getProgress();
int getDuration(); int getDuration();

View file

@ -170,7 +170,7 @@ public final class PreferenceUtil {
} }
public int getPosition() { public int getPosition() {
return mPreferences.getInt(POSITION, -1); return mPreferences.getInt(POSITION, 0);
} }
public void setPosition(int position) { public void setPosition(int position) {
@ -178,7 +178,7 @@ public final class PreferenceUtil {
} }
public int getProgress() { public int getProgress() {
return mPreferences.getInt(PROGRESS, -1); return mPreferences.getInt(PROGRESS, 0);
} }
public void setProgress(int progress) { public void setProgress(int progress) {