From 5c45c4540ab72463492683b68c17bbad68d3cc0b Mon Sep 17 00:00:00 2001 From: Christopher Eby Date: Sun, 18 Sep 2011 08:54:48 -0500 Subject: [PATCH] Add random option Disabling this option makes Vanilla function more like a typical music player. --- res/values/arrays.xml | 2 + res/values/strings.xml | 9 +- src/org/kreed/vanilla/CoverView.java | 52 +- .../kreed/vanilla/FullPlaybackActivity.java | 76 +-- src/org/kreed/vanilla/PlaybackActivity.java | 50 +- src/org/kreed/vanilla/PlaybackService.java | 221 +++++---- src/org/kreed/vanilla/Song.java | 19 +- src/org/kreed/vanilla/SongSelector.java | 10 +- src/org/kreed/vanilla/SongTimeline.java | 460 ++++++++++-------- 9 files changed, 509 insertions(+), 390 deletions(-) diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 09307ca9..35230837 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -50,6 +50,7 @@ THE SOFTWARE. Previous Song Toggle Repeat Toggle Shuffle + Toggle Random Enqueue Current Album Enqueue Current Artist Enqueue Current Genre @@ -70,5 +71,6 @@ THE SOFTWARE. 8 9 10 + 11 diff --git a/res/values/strings.xml b/res/values/strings.xml index f21b119e..b56901e0 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -25,18 +25,23 @@ THE SOFTWARE. No songs found on your device. + No songs selected. Pick some from the library (search key) or enter random mode (in the menu). Preferences Library Stop and Exit Display Mode Enable Shuffle Disable Shuffle - Shuffle enabled for newly added songs + Shuffle enabled Shuffle disabled Enable Repeat Disable Repeat - Repeat enabled. The current song and any songs you have added after it will be repeated. + Repeat enabled Repeat disabled + Enable Random + Disable Random + Random enabled + Random disabled Failed to load song %s. It may be corrupt or missing. Queue cleared. diff --git a/src/org/kreed/vanilla/CoverView.java b/src/org/kreed/vanilla/CoverView.java index fdcad9f8..83e6f789 100644 --- a/src/org/kreed/vanilla/CoverView.java +++ b/src/org/kreed/vanilla/CoverView.java @@ -76,7 +76,6 @@ public final class CoverView extends View implements Handler.Callback { */ private Cache mBitmapCache = new Cache(8); - private int mTimelinePos; private Scroller mScroller; private VelocityTracker mVelocityTracker; private float mLastMotionX; @@ -117,23 +116,6 @@ public final class CoverView extends View implements Handler.Callback { mCoverStyle = style; } - /** - * Move to the next or previous song. - * - * @param delta -1 or 1, indicate the previous or next song, respectively - */ - private void go(int delta) - { - int i = delta > 0 ? STORE_SIZE - 1 : 0; - int from = delta > 0 ? 1 : 0; - int to = delta > 0 ? 0 : 1; - System.arraycopy(mSongs, from, mSongs, to, STORE_SIZE - 1); - mSongs[i] = null; - - resetScroll(); - invalidate(); - } - /** * Reset the scroll position to its default state. */ @@ -240,7 +222,7 @@ public final class CoverView extends View implements Handler.Callback { if (Math.abs(deltaX) > Math.abs(deltaY)) { if (deltaX < 0) { - int availableToScroll = scrollX - (mTimelinePos == 0 ? width : 0); + int availableToScroll = scrollX - (mSongs[0] == null ? width : 0); if (availableToScroll > 0) scrollBy(Math.max(-availableToScroll, (int)deltaX), 0); } else if (deltaX > 0) { @@ -264,7 +246,7 @@ public final class CoverView extends View implements Handler.Callback { velocityTracker.computeCurrentVelocity(250); int velocity = (int) velocityTracker.getXVelocity(); - int min = mTimelinePos == 0 ? 1 : 0; + int min = mSongs[0] == null ? 1 : 0; int max = 2; int nearestCover = (scrollX + width / 2) / width; int whichCover = Math.max(min, Math.min(nearestCover, max)); @@ -364,33 +346,15 @@ public final class CoverView extends View implements Handler.Callback { } /** - * Query current Song for all positions with null songs. - * - * @param force Query all songs, even those that are non-null + * Query all songs. Must be called on the UI thread. */ - private void querySongs(boolean force) + public void querySongs() { PlaybackService service = ContextApplication.getService(); - for (int i = STORE_SIZE; --i != -1; ) { - if (force || mSongs[i] == null) - setSong(i, service.getSong(i - STORE_SIZE / 2)); - } - } - - public void setCurrentSong(Song song) - { - mTimelinePos = ContextApplication.getService().getTimelinePos(); - - for (int delta = -STORE_SIZE / 2; delta <= STORE_SIZE / 2; ++delta) { - if (mSongs[delta + STORE_SIZE / 2] == song) { - if (delta != 0) - go(delta); - querySongs(false); - return; - } - } - - querySongs(true); + for (int i = STORE_SIZE; --i != -1; ) + setSong(i, service.getSong(i - STORE_SIZE / 2)); + resetScroll(); + invalidate(); } /** diff --git a/src/org/kreed/vanilla/FullPlaybackActivity.java b/src/org/kreed/vanilla/FullPlaybackActivity.java index 31cb2707..478c69c4 100644 --- a/src/org/kreed/vanilla/FullPlaybackActivity.java +++ b/src/org/kreed/vanilla/FullPlaybackActivity.java @@ -22,7 +22,6 @@ package org.kreed.vanilla; -import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; import android.os.Bundle; @@ -30,9 +29,11 @@ import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; import android.util.Log; +import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; @@ -53,6 +54,7 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On private Handler mUiHandler = new Handler(this); private RelativeLayout mMessageOverlay; + private TextView mOverlayText; private View mControlsTop; private View mControlsBottom; @@ -146,24 +148,6 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On mPaused = true; } - /** - * Show the message view overlay, creating it if necessary and clearing - * it of all content. - */ - private void showMessageOverlay() - { - if (mMessageOverlay == null) { - mMessageOverlay = new RelativeLayout(this); - mMessageOverlay.setBackgroundColor(Color.BLACK); - addContentView(mMessageOverlay, - new ViewGroup.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, - LinearLayout.LayoutParams.FILL_PARENT)); - } else { - mMessageOverlay.setVisibility(View.VISIBLE); - mMessageOverlay.removeAllViews(); - } - } - /** * Hide the message overlay, if it exists. */ @@ -174,21 +158,44 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On } /** - * Show the no media message in the message overlay. The message overlay - * must have been created with showMessageOverlay before this method is - * called. + * Show some text in a message overlay. + * + * @param text Resource id of the text to show. */ - private void setNoMediaOverlayMessage() + private void showOverlayMessage(int text) { - RelativeLayout.LayoutParams layoutParams = - new RelativeLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT); - layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); + if (mMessageOverlay == null) { + mMessageOverlay = new RelativeLayout(this) { + @Override + public boolean onTouchEvent(MotionEvent ev) + { + // Eat all touch events so they don't pass through to the + // CoverView + return true; + } + }; - TextView text = new TextView(this); - text.setText(R.string.no_songs); - text.setLayoutParams(layoutParams); - mMessageOverlay.addView(text); + mMessageOverlay.setBackgroundColor(Color.BLACK); + + RelativeLayout.LayoutParams layoutParams = + new RelativeLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); + layoutParams.setMargins(20, 20, 20, 20); + + mOverlayText = new TextView(this); + mOverlayText.setLayoutParams(layoutParams); + mOverlayText.setGravity(Gravity.CENTER); + mMessageOverlay.addView(mOverlayText); + + addContentView(mMessageOverlay, + new ViewGroup.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, + LinearLayout.LayoutParams.FILL_PARENT)); + } else { + mMessageOverlay.setVisibility(View.VISIBLE); + } + + mOverlayText.setText(text); } @Override @@ -196,10 +203,11 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On { super.onStateChange(state, toggled); - if ((toggled & PlaybackService.FLAG_NO_MEDIA) != 0) { + if ((toggled & (PlaybackService.FLAG_NO_MEDIA|PlaybackService.FLAG_EMPTY_QUEUE)) != 0) { if ((state & PlaybackService.FLAG_NO_MEDIA) != 0) { - showMessageOverlay(); - setNoMediaOverlayMessage(); + showOverlayMessage(R.string.no_songs); + } else if ((state & PlaybackService.FLAG_EMPTY_QUEUE) != 0) { + showOverlayMessage(R.string.empty_queue); } else { hideMessageOverlay(); } diff --git a/src/org/kreed/vanilla/PlaybackActivity.java b/src/org/kreed/vanilla/PlaybackActivity.java index 9410da8d..9a109bc9 100644 --- a/src/org/kreed/vanilla/PlaybackActivity.java +++ b/src/org/kreed/vanilla/PlaybackActivity.java @@ -47,10 +47,11 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View public static final int ACTION_PREVIOUS_SONG = 4; public static final int ACTION_REPEAT = 5; public static final int ACTION_SHUFFLE = 6; - public static final int ACTION_ENQUEUE_ALBUM = 7; - public static final int ACTION_ENQUEUE_ARTIST = 8; - public static final int ACTION_ENQUEUE_GENRE = 9; - public static final int ACTION_CLEAR_QUEUE = 10; + public static final int ACTION_RANDOM = 7; + public static final int ACTION_ENQUEUE_ALBUM = 8; + public static final int ACTION_ENQUEUE_ARTIST = 9; + public static final int ACTION_ENQUEUE_GENRE = 10; + public static final int ACTION_CLEAR_QUEUE = 11; public static int mUpAction; public static int mDownAction; @@ -175,7 +176,7 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View public void playPause() { PlaybackService service = ContextApplication.getService(); - int state = service.toggleFlag(PlaybackService.FLAG_PLAYING); + int state = service.playPause(); if ((state & PlaybackService.FLAG_ERROR) != 0) Toast.makeText(this, service.getErrorMessage(), Toast.LENGTH_LONG).show(); setState(state); @@ -243,7 +244,7 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View protected void onSongChange(Song song) { if (mCoverView != null) - mCoverView.setCurrentSong(song); + mCoverView.querySongs(); } protected void setSong(final Song song) @@ -298,6 +299,7 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View static final int MENU_PLAYBACK = 5; static final int MENU_REPEAT = 6; static final int MENU_SEARCH = 7; + static final int MENU_RANDOM = 8; @Override public boolean onCreateOptionsMenu(Menu menu) @@ -305,6 +307,7 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View menu.add(0, MENU_PREFS, 0, R.string.settings).setIcon(R.drawable.ic_menu_preferences); menu.add(0, MENU_SHUFFLE, 0, R.string.shuffle_enable).setIcon(R.drawable.ic_menu_shuffle); menu.add(0, MENU_REPEAT, 0, R.string.repeat_enable).setIcon(R.drawable.ic_menu_refresh); + menu.add(0, MENU_RANDOM, 0, R.string.random_enable).setIcon(R.drawable.ic_menu_shuffle); menu.add(0, MENU_QUIT, 0, R.string.quit).setIcon(R.drawable.ic_menu_close_clear_cancel); return true; } @@ -312,12 +315,14 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View @Override public boolean onPrepareOptionsMenu(Menu menu) { - boolean isShuffling = (mState & PlaybackService.FLAG_SHUFFLE) != 0; + int state = mState; + boolean isShuffling = (state & PlaybackService.FLAG_SHUFFLE) != 0; menu.findItem(MENU_SHUFFLE).setTitle(isShuffling ? R.string.shuffle_disable : R.string.shuffle_enable); - boolean isRepeating = (mState & PlaybackService.FLAG_REPEAT) != 0; + boolean isRepeating = (state & PlaybackService.FLAG_REPEAT) != 0; menu.findItem(MENU_REPEAT).setTitle(isRepeating ? R.string.repeat_disable : R.string.repeat_enable); - if ((mState & PlaybackService.FLAG_NO_MEDIA) != 0) - menu.findItem(MENU_REPEAT).setEnabled(false); + boolean isRandom = (state & PlaybackService.FLAG_RANDOM) != 0; + // TODO: find icon (dice? arrow pointing in many directions?) + menu.findItem(MENU_RANDOM).setTitle(isRandom ? R.string.random_disable : R.string.random_enable); return true; } @@ -334,6 +339,9 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View case MENU_REPEAT: toggleRepeat(); return true; + case MENU_RANDOM: + toggleRandom(); + return true; case MENU_PREFS: startActivity(new Intent(this, PreferencesActivity.class)); return true; @@ -353,9 +361,9 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View */ public void toggleShuffle() { - int state = ContextApplication.getService().toggleFlag(PlaybackService.FLAG_SHUFFLE); + int state = ContextApplication.getService().toggleShuffle(); int res = (state & PlaybackService.FLAG_SHUFFLE) == 0 ? R.string.shuffle_disabling : R.string.shuffle_enabling; - Toast.makeText(this, res, Toast.LENGTH_LONG).show(); + Toast.makeText(this, res, Toast.LENGTH_SHORT).show(); setState(state); } @@ -364,9 +372,20 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View */ public void toggleRepeat() { - int state = ContextApplication.getService().toggleFlag(PlaybackService.FLAG_REPEAT); + int state = ContextApplication.getService().toggleRepeat(); int res = (state & PlaybackService.FLAG_REPEAT) == 0 ? R.string.repeat_disabling : R.string.repeat_enabling; - Toast.makeText(this, res, Toast.LENGTH_LONG).show(); + Toast.makeText(this, res, Toast.LENGTH_SHORT).show(); + setState(state); + } + + /** + * Toggle random mode on/off + */ + public void toggleRandom() + { + int state = ContextApplication.getService().toggleRandom(); + int res = (state & PlaybackService.FLAG_RANDOM) == 0 ? R.string.random_disabling : R.string.random_enabling; + Toast.makeText(this, res, Toast.LENGTH_SHORT).show(); setState(state); } @@ -408,6 +427,9 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View case ACTION_SHUFFLE: toggleShuffle(); break; + case ACTION_RANDOM: + toggleRandom(); + break; case ACTION_ENQUEUE_ALBUM: enqueue(MediaUtils.TYPE_ALBUM); break; diff --git a/src/org/kreed/vanilla/PlaybackService.java b/src/org/kreed/vanilla/PlaybackService.java index 2bce2377..4d9f453d 100644 --- a/src/org/kreed/vanilla/PlaybackService.java +++ b/src/org/kreed/vanilla/PlaybackService.java @@ -99,14 +99,37 @@ public final class PlaybackService extends Service implements Handler.Callback, public static final String EVENT_REPLACE_SONG = "org.kreed.vanilla.event.REPLACE_SONG"; public static final String EVENT_CHANGED = "org.kreed.vanilla.event.CHANGED"; + /** + * Set when there is no media available on the device. + */ public static final int FLAG_NO_MEDIA = 0x2; + /** + * If set, music will play. + */ public static final int FLAG_PLAYING = 0x1; + /** + * If set, songs selected from the library and repeated songs will be in + * random order. + */ public static final int FLAG_SHUFFLE = 0x4; + /** + * If set, will loop back to the beginning of the timeline when its end is + * reached. + */ public static final int FLAG_REPEAT = 0x8; /** * Set when the current song is unplayable. */ public static final int FLAG_ERROR = 0x10; + /** + * If set, random songs will be added to the timeline when its end is + * reached. Overrides FLAG_REPEAT. + */ + public static final int FLAG_RANDOM = 0x20; + /** + * Set when the user needs to select songs to play. + */ + public static final int FLAG_EMPTY_QUEUE = 0x40; public static final int NEVER = 0; public static final int WHEN_PLAYING = 1; @@ -194,7 +217,10 @@ public final class PlaybackService extends Service implements Handler.Callback, initWidgets(); int state = 0; - if (mTimeline.isRepeating()) + int finishAction = mTimeline.getFinishAction(); + if (finishAction == SongTimeline.FINISH_RANDOM) + state |= FLAG_RANDOM; + else if (finishAction == SongTimeline.FINISH_REPEAT) state |= FLAG_REPEAT; if (mTimeline.isShuffling()) state |= FLAG_SHUFFLE; @@ -335,14 +361,6 @@ public final class PlaybackService extends Service implements Handler.Callback, } } - private void broadcastReplaceSong(int delta, Song song) - { - Intent intent = new Intent(EVENT_REPLACE_SONG); - intent.putExtra("pos", delta); - intent.putExtra("song", song); - mHandler.sendMessage(mHandler.obtainMessage(BROADCAST, intent)); - } - private void setFlag(int flag) { synchronized (mStateLock) { @@ -373,7 +391,7 @@ public final class PlaybackService extends Service implements Handler.Callback, */ private int updateState(int state) { - if ((state & FLAG_NO_MEDIA) != 0 || (state & FLAG_ERROR) != 0 || (mHeadsetOnly && isSpeakerOn())) + if ((state & (FLAG_NO_MEDIA|FLAG_ERROR|FLAG_EMPTY_QUEUE)) != 0 || (mHeadsetOnly && isSpeakerOn())) state &= ~FLAG_PLAYING; int oldState = mState; @@ -423,8 +441,16 @@ public final class PlaybackService extends Service implements Handler.Callback, if ((toggled & FLAG_SHUFFLE) != 0) mTimeline.setShuffle((state & FLAG_SHUFFLE) != 0); - if ((toggled & FLAG_REPEAT) != 0) - mTimeline.setRepeat((state & FLAG_REPEAT) != 0); + if ((toggled & (FLAG_REPEAT | FLAG_RANDOM)) != 0) { + int action; + if ((state & FLAG_RANDOM) != 0) + action = SongTimeline.FINISH_RANDOM; + else if ((state & FLAG_REPEAT) != 0) + action = SongTimeline.FINISH_REPEAT; + else + action = SongTimeline.FINISH_STOP; + mTimeline.setFinishAction(action); + } } private void broadcastChange(int state, Song song, long uptime) @@ -490,19 +516,57 @@ public final class PlaybackService extends Service implements Handler.Callback, } /** - * Toggle a flag in the state on or off + * If playing, pause. If paused, play. * - * @param flag The flag to be toggled - * @return The new state + * @return The new state after this is called. */ - public int toggleFlag(int flag) + public int playPause() { - int state; synchronized (mStateLock) { - state = updateState(mState ^ flag); + userActionTriggered(); + // If trying to play with empty queue, enter random mode. + if ((mState & FLAG_PLAYING) == 0 && (mState & FLAG_EMPTY_QUEUE) != 0) { + updateState((mState | FLAG_RANDOM) & ~FLAG_REPEAT); + setCurrentSong(0); + } + return updateState(mState ^ FLAG_PLAYING); + } + } + + /** + * Toggle random mode. Disables repeat mode. + * + * @return The new state after this is called. + */ + public int toggleRandom() + { + synchronized (mStateLock) { + return updateState((mState ^ FLAG_RANDOM) & ~FLAG_REPEAT); + } + } + + /** + * Toggle repeat mode. Disables random mode. + * + * @return The new state after this is called. + */ + public int toggleRepeat() + { + synchronized (mStateLock) { + return updateState((mState ^ FLAG_REPEAT) & ~FLAG_RANDOM); + } + } + + /** + * Toggle shuffle mode. + * + * @return The new state after this is called. + */ + public int toggleShuffle() + { + synchronized (mStateLock) { + return updateState(mState ^ FLAG_SHUFFLE); } - userActionTriggered(); - return state; } /** @@ -524,13 +588,28 @@ public final class PlaybackService extends Service implements Handler.Callback, mCurrentSong = song; if (song == null) { if (Song.isSongAvailable()) { - return setCurrentSong(+1); // we only encountered a bad song; skip it + // We either have a stale song id or have no songs in the + // the timeline. Clear the timeline to get rid of the possible + // stale song id. + mTimeline.clear(); + + if ((mState & FLAG_RANDOM) == 0) { + // Tell the user to pick some songs. + setFlag(FLAG_EMPTY_QUEUE); + return null; + } else { + // Add a random song to replace the stale one. + return setCurrentSong(0); + } } else { - setFlag(FLAG_NO_MEDIA); // we don't have any songs : / + // we don't have any songs : / + setFlag(FLAG_NO_MEDIA); return null; } } else if ((mState & FLAG_NO_MEDIA) != 0) { unsetFlag(FLAG_NO_MEDIA); + } else if ((mState & FLAG_EMPTY_QUEUE) != 0) { + unsetFlag(FLAG_EMPTY_QUEUE); } mHandler.removeMessages(PROCESS_SONG); @@ -574,17 +653,19 @@ public final class PlaybackService extends Service implements Handler.Callback, updateNotification(); - getSong(+2); mTimeline.purge(); - mHandler.removeMessages(SAVE_STATE); - mHandler.sendEmptyMessageDelayed(SAVE_STATE, 5000); } + @Override public void onCompletion(MediaPlayer player) { - setCurrentSong(+1); + if (mTimeline.isEndOfQueue()) + unsetFlag(FLAG_PLAYING); + else + setCurrentSong(+1); } + @Override public boolean onError(MediaPlayer player, int what, int extra) { Log.e("VanillaMusic", "MediaPlayer error: " + what + " " + extra); @@ -615,7 +696,7 @@ public final class PlaybackService extends Service implements Handler.Callback, } if (delta == 0) - toggleFlag(FLAG_PLAYING); + playPause(); else setCurrentSong(delta); } @@ -807,16 +888,6 @@ public final class PlaybackService extends Service implements Handler.Callback, } } - /** - * Returns the position of the current song in the song timeline. - * - * @see SongTimeline#getCurrentPosition() - */ - public int getTimelinePos() - { - return mTimeline.getCurrentPosition(); - } - /** * Seek to a position in the current song. * @@ -838,12 +909,16 @@ public final class PlaybackService extends Service implements Handler.Callback, return null; } - /** - * Notify clients that a song in the timeline has been replaced. - */ - public void songReplaced(int delta, Song song) + @Override + public void activeSongReplaced(int delta, Song song) { - broadcastReplaceSong(delta, song); + if (delta == 0) + setCurrentSong(0); + + Intent intent = new Intent(EVENT_REPLACE_SONG); + intent.putExtra("pos", delta); + intent.putExtra("song", song); + mHandler.sendMessage(mHandler.obtainMessage(BROADCAST, intent)); } /** @@ -855,9 +930,7 @@ public final class PlaybackService extends Service implements Handler.Callback, */ public void removeSong(long id) { - boolean shouldAdvance = mTimeline.removeSong(id); - if (shouldAdvance) - setCurrentSong(0); + mTimeline.removeSong(id); } /** @@ -901,43 +974,18 @@ public final class PlaybackService extends Service implements Handler.Callback, } /** - * Given a song or group of songs represented by the given type and id, play - * the first and enqueues the rest after it. - * - * If FLAG_SHUFFLE is enabled, songs will be added to the song timeline in - * random order, otherwise, songs will be ordered by album name and then - * track number. + * Add a song or group of songs represented by the given type and id to the + * timeline. * + * @param mode One of SongTimeline.MODE_*. Tells whether to play the songs + * immediately or enqueue them for later. * @param type The media type, one of MediaUtils.TYPE_* * @param id The MediaStore id of the media * @return The number of songs that were enqueued. */ - public int playSongs(int type, long id) + public int addSongs(int mode, int type, long id) { - int count = mTimeline.chooseSongs(false, type, id, null); - setCurrentSong(+1); - return count; - } - - /** - * Enqueues a song or group of songs represented by the given type and id. - * - * The first song from the group will be placed in the timeline either - * after the last enqueued song or after the playing song if the queue is - * empty. If FLAG_SHUFFLE is enabled, songs will be added to the song - * timeline in random order, otherwise, songs will be ordered by album name - * and then track number. - * - * @param type The media type, one of MediaUtils.TYPE_* - * @param id The MediaStore id of the media - * @return The number of songs that were enqueued. - */ - public int enqueueSongs(int type, long id) - { - int count = mTimeline.chooseSongs(true, type, id, null); - mHandler.removeMessages(SAVE_STATE); - mHandler.sendEmptyMessageDelayed(SAVE_STATE, 5000); - return count; + return mTimeline.addSongs(mode, type, id, null); } /** @@ -957,7 +1005,7 @@ public final class PlaybackService extends Service implements Handler.Callback, if (current == null) return 0; - long id = -1; + long id; switch (type) { case MediaUtils.TYPE_ARTIST: id = current.artistId; @@ -973,8 +1021,7 @@ public final class PlaybackService extends Service implements Handler.Callback, } String selection = "_id!=" + current.id; - int count = mTimeline.chooseSongs(false, type, id, selection); - return count; + return mTimeline.addSongs(SongTimeline.MODE_PLAY_NEXT, type, id, selection); } /** @@ -985,15 +1032,6 @@ public final class PlaybackService extends Service implements Handler.Callback, mTimeline.clearQueue(); } - /** - * Reset the position at which songs are enqueued. That is, the next song - * enqueued will be placed directly after the playing song. - */ - public void finishEnqueueing() - { - mTimeline.finishEnqueueing(); - } - /** * Return the error message set when FLAG_ERROR is set. */ @@ -1001,4 +1039,11 @@ public final class PlaybackService extends Service implements Handler.Callback, { return mErrorMessage; } + + @Override + public void timelineChanged() + { + mHandler.removeMessages(SAVE_STATE); + mHandler.sendEmptyMessageDelayed(SAVE_STATE, 5000); + } } diff --git a/src/org/kreed/vanilla/Song.java b/src/org/kreed/vanilla/Song.java index 49b641c7..71d5572a 100644 --- a/src/org/kreed/vanilla/Song.java +++ b/src/org/kreed/vanilla/Song.java @@ -44,7 +44,7 @@ public class Song implements Parcelable { /** * Indicates that this song was randomly selected from all songs. */ - public static final int FLAG_RANDOM = 0x1; + private static final int FLAG_RANDOM = 0x1; /** * A cache of covers that have been loaded with getCover(). @@ -200,8 +200,7 @@ public class Song implements Parcelable { public static void onMediaChange() { mSongCount = -1; - // TODO: make reload optional? - mAllSongs = loadAllSongs(); + mAllSongs = null; } /** @@ -297,6 +296,14 @@ public class Song implements Parcelable { this.flags = flags; } + /** + * Return true if this song was retrieved from randomSong(). + */ + public boolean isRandom() + { + return (flags & FLAG_RANDOM) != 0; + } + /** * Populate fields with data from the supplied cursor. * @@ -464,4 +471,10 @@ public class Song implements Parcelable { return cover; } + + @Override + public String toString() + { + return String.format("%d %s", id, path); + } } diff --git a/src/org/kreed/vanilla/SongSelector.java b/src/org/kreed/vanilla/SongSelector.java index 0bc29468..0d902142 100644 --- a/src/org/kreed/vanilla/SongSelector.java +++ b/src/org/kreed/vanilla/SongSelector.java @@ -205,7 +205,6 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem mTextFilter.setText(""); setSearchBoxVisible(false); } else { - ContextApplication.getService().finishEnqueueing(); finish(); } break; @@ -247,7 +246,7 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem PlaybackService service = ContextApplication.getService(); int type = view.getMediaType(); long id = view.getMediaId(); - int count; + int mode; int text; if (action == ACTION_LAST_USED) @@ -257,18 +256,20 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem switch (action) { case ACTION_PLAY: - count = service.playSongs(type, id); + mode = SongTimeline.MODE_PLAY; text = R.plurals.playing; break; case ACTION_ENQUEUE: - count = service.enqueueSongs(type, id); + mode = SongTimeline.MODE_ENQUEUE; text = R.plurals.enqueued; break; default: return; } + int count = service.addSongs(mode, type, id); setSong(service.getSong(0)); + Toast.makeText(this, getResources().getQuantityString(text, count, count), Toast.LENGTH_SHORT).show(); mLastActedId = id; } @@ -574,7 +575,6 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem setSearchBoxVisible(!mSearchBoxVisible); return true; case MENU_PLAYBACK: - ContextApplication.getService().finishEnqueueing(); startActivity(new Intent(this, FullPlaybackActivity.class)); return true; default: diff --git a/src/org/kreed/vanilla/SongTimeline.java b/src/org/kreed/vanilla/SongTimeline.java index a19b980a..e68d3133 100644 --- a/src/org/kreed/vanilla/SongTimeline.java +++ b/src/org/kreed/vanilla/SongTimeline.java @@ -39,6 +39,44 @@ import android.util.Log; * exist at a requested position. */ public final class SongTimeline { + /** + * Stop playback. + * + * @see SongTimeline#setFinishAction(int) + */ + public static final int FINISH_STOP = 0; + /** + * Repeat from the begining. + * + * @see SongTimeline#setFinishAction(int) + */ + public static final int FINISH_REPEAT = 1; + /** + * Add random songs to the playlist. + * + * @see SongTimeline#setFinishAction(int) + */ + public static final int FINISH_RANDOM = 2; + + /** + * Clear the timeline and use only the provided songs. + * + * @see SongTimeline#addSongs(int,int,long,String) + */ + public static final int MODE_PLAY = 0; + /** + * Clear the queue and add the songs after the current song. + * + * @see SongTimeline#addSongs(int,int,long,String) + */ + public static final int MODE_PLAY_NEXT = 1; + /** + * Add the songs at the end of the timeline, clearing random songs. + * + * @see SongTimeline#addSongs(int,int,long,String) + */ + public static final int MODE_ENQUEUE = 2; + /** * Name of the state file. */ @@ -47,7 +85,7 @@ public final class SongTimeline { * Header for state file to help indicate if the file is in the right * format. */ - private static final long STATE_FILE_MAGIC = 0x8a9d3f2fca33L; + private static final long STATE_FILE_MAGIC = 0x8f9d3a2fca33L; /** * All the songs currently contained in the timeline. Each Song object @@ -58,38 +96,41 @@ public final class SongTimeline { * The position of the current song (i.e. the playing song). */ private int mCurrentPos; - /** - * The distance from mCurrentPos at which songs will be enqueued by - * chooseSongs. If 0, then songs will be enqueued at the position - * immediately following the current song. If 1, there will be one - * position between them, etc. - */ - private int mQueueOffset; - /** - * When a repeat is engaged, this position will be rewound to. - */ - private int mRepeatStart = -1; - /** - * The songs that will be repeated on the next repeat. We cache these so - * that, if they need to be shuffled, they are only shuffled once. - */ - private ArrayList mRepeatedSongs; /** * Whether shuffling is enabled. Shuffling will shuffle sets of songs * that are added with chooseSongs and shuffle sets of repeated songs. */ private boolean mShuffle; + /** + * What to do when the end of the playlist is reached. + * Must be one of SongTimeline.FINISH_*. + */ + private int mFinishAction; + + // for shuffleAll() + private ArrayList mShuffledSongs; + + // for saveActiveSongs() + private Song mSavedPrevious; + private Song mSavedCurrent; + private Song mSavedNext; public interface Callback { /** - * Called when a song in the timeline has been replaced with a - * different song. + * Called when an active song in the timeline is replaced by a method + * other than shiftCurrentSong() * * @param delta The distance from the current song. Will always be -1, * 0, or 1. * @param song The new song at the position */ - public void songReplaced(int delta, Song song); + public void activeSongReplaced(int delta, Song song); + + /** + * Called when the timeline state has changed and should be saved to + * storage. + */ + public void timelineChanged(); } /** * The current Callback, if any. @@ -99,7 +140,7 @@ public final class SongTimeline { /** * Initializes the timeline with the state stored in the state file created * by a call to save state. - * + * * @param context The Context to open the state file with * @return The optional extra data, or -1 if loading failed */ @@ -113,13 +154,13 @@ public final class SongTimeline { int n = in.readInt(); if (n > 0) { ArrayList songs = new ArrayList(n); - + for (int i = 0; i != n; ++i) songs.add(new Song(in.readLong(), in.readInt())); - + mSongs = songs; mCurrentPos = in.readInt(); - mRepeatStart = in.readInt(); + mFinishAction = in.readInt(); mShuffle = in.readBoolean(); extra = in.readInt(); } @@ -167,7 +208,7 @@ public final class SongTimeline { } out.writeInt(mCurrentPos); - out.writeInt(mRepeatStart); + out.writeInt(mFinishAction); out.writeBoolean(mShuffle); out.writeInt(extra); } @@ -195,21 +236,13 @@ public final class SongTimeline { } /** - * Return whether repeating is enabled. + * Return the finish action. + * + * @see SongTimeline#setFinishAction(int) */ - public boolean isRepeating() + public int getFinishAction() { - return mRepeatStart != -1; - } - - /** - * Return the position of the current song (i.e. the playing song). - */ - public int getCurrentPosition() - { - synchronized (this) { - return mCurrentPos; - } + return mFinishAction; } /** @@ -218,123 +251,90 @@ public final class SongTimeline { */ public void setShuffle(boolean shuffle) { + saveActiveSongs(); mShuffle = shuffle; + broadcastChangedSongs(); + changed(); } /** - * Set whether to repeat. - * - * When called with true, repeat will be enabled and the current song will - * become the starting point for repeats, where the position is rewound to - * when a repeat is engaged. A repeat is engaged when a randomly selected - * song is encountered (i.e. a non-user-chosen song). - * - * The current song must be non-null. + * Set what to do when the end of the playlist is reached. Must be one of + * SongTimeline.FINISH_* (stop, repeat, or add random song). */ - public void setRepeat(boolean repeat) + public void setFinishAction(int action) { - // Don't change anything if we are already doing what we want. - if (repeat == (mRepeatStart != -1)) - return; - - synchronized (this) { - if (repeat) { - Song song = getSong(0); - if (song == null) - return; - mRepeatStart = mCurrentPos; - // Ensure that we will at least repeat one song (the current song), - // even if all of our songs were selected randomly. - song.flags &= ~Song.FLAG_RANDOM; - } else { - mRepeatStart = -1; - mRepeatedSongs = null; - } - - if (mCallback != null) - mCallback.songReplaced(+1, getSong(+1)); - } + saveActiveSongs(); + mFinishAction = action; + broadcastChangedSongs(); + changed(); } /** - * Retrieves a shuffled list of the songs to be repeated. This caches the - * results so that the repeated songs are shuffled only once. + * Shuffle all the songs in the timeline, storing the result in + * mShuffledSongs. * - * @param end The position just after the last song to be included in the - * repeated songs + * @return The first song from the shuffled songs. */ - private ArrayList getShuffledRepeatedSongs(int end) + private Song shuffleAll() { - if (mRepeatedSongs == null) { - ArrayList songs = new ArrayList(mSongs.subList(mRepeatStart, end)); - Collections.shuffle(songs, ContextApplication.getRandom()); - mRepeatedSongs = songs; - } - return mRepeatedSongs; + ArrayList songs = new ArrayList(mSongs); + Collections.shuffle(songs, ContextApplication.getRandom()); + mShuffledSongs = songs; + return songs.get(0); } /** * Returns the song delta places away from the current - * position. If there is no song at the given position, a random - * song will be placed in that position. Returns null if there is a problem - * retrieving the song (caused by either an empty library or stale song id). + * position. Returns null if there is a problem retrieving the song + * (caused by either an empty library or stale song id). * - * Note: This returns songs based on their position in the playback - * sequence, not their position in the stored timeline. When repeat is enabled, - * the two will differ. - * - * @param delta The offset from the current position. Should be -1, 0, or - * 1. + * @param delta The offset from the current position. Must be -1, 0, or 1. */ public Song getSong(int delta) { + assert(delta >= -1 && delta <= 1); + ArrayList timeline = mSongs; Song song = null; synchronized (this) { int pos = mCurrentPos + delta; - if (pos < 0) - return null; + int size = timeline.size(); - while (pos >= timeline.size()) { - song = Song.randomSong(); - if (song == null) + if (pos < 0) { + if (size == 0 || mFinishAction == FINISH_RANDOM) return null; - timeline.add(song); - } - - if (song == null) - song = timeline.get(pos); - - if (song != null && mRepeatStart != -1 && (song.flags & Song.FLAG_RANDOM) != 0) { - if (delta == 1 && mRepeatStart < mCurrentPos + 1) { - // We have reached a non-user-selected song; this song will - // repeated in shiftCurrentSong so take alternative - // measures - if (mShuffle) - song = getShuffledRepeatedSongs(mCurrentPos + 1).get(0); + song = timeline.get(Math.max(0, size - 1)); + } else if (pos > size) { + return null; + } else if (pos == size) { + switch (mFinishAction) { + case FINISH_STOP: + case FINISH_REPEAT: + if (size == 0) + // empty queue + return null; + else if (mShuffle) + song = shuffleAll(); else - song = timeline.get(mRepeatStart); - } else if (delta == 0 && mRepeatStart < mCurrentPos) { - // We have just been set to a position after the repeat - // where a repeat is necessary. Rewind to the repeat - // start, shuffling if needed - if (mShuffle) { - int j = mCurrentPos; - ArrayList songs = getShuffledRepeatedSongs(j); - for (int i = songs.size(); --i != -1 && --j != -1; ) - timeline.set(j, songs.get(i)); - mRepeatedSongs = null; - } - - mCurrentPos = mRepeatStart; - song = timeline.get(mRepeatStart); - if (mCallback != null) - mCallback.songReplaced(-1, getSong(-1)); + song = timeline.get(0); + break; + case FINISH_RANDOM: + song = Song.randomSong(); + timeline.add(song); + break; + default: + throw new IllegalStateException("Invalid finish action: " + mFinishAction); } + } else { + song = timeline.get(pos); } } + if (song == null) + // we have no songs in the library + return null; + if (!song.query(false)) // we have a stale song id return null; @@ -345,30 +345,45 @@ public final class SongTimeline { /** * Shift the current song by delta places. * + * @param delta The delta. Must be -1, 0, 1. * @return The Song at the new position */ public Song shiftCurrentSong(int delta) { + assert(delta >= -1 && delta <= 1); + synchronized (this) { - mCurrentPos += delta; - if (mQueueOffset > 0) - mQueueOffset -= 1; - return getSong(0); + int pos = mCurrentPos + delta; + + if (mFinishAction != FINISH_RANDOM && pos == mSongs.size()) { + if (mShuffle && mSongs.size() > 0) { + if (mShuffledSongs == null) + shuffleAll(); + mSongs = mShuffledSongs; + } + + pos = 0; + } else if (pos < 0) { + if (mFinishAction == FINISH_RANDOM) + pos = 0; + else + pos = Math.max(0, mSongs.size() - 1); + } + + mCurrentPos = pos; + mShuffledSongs = null; } + + if (delta != 0) + changed(); + + return getSong(0); } /** - * Add a set of songs to the song timeline. There are two modes: play and - * enqueue. Play will place the set immediately after the current song. (It - * is assumed that client code will shift the current position and play the - * first song of the set after a call to play). Enqueue will place the set - * after the last enqueued song or after the currently playing song if no - * items have been enqueued since the last call to finishEnqueueing. + * Add a set of songs to the song timeline. * - * If shuffling is enabled, songs will be in random order. Otherwise songs - * will be ordered by album and then by track number. - * - * @param enqueue If true, enqueue the set. If false, play the set. + * @param mode How to add the songs. One of SongTimeline.MODE_*. * @param type The type represented by the id. Must be one of the * MediaUtils.FIELD_* constants. * @param id The id of the element in the MediaStore content provider for @@ -377,7 +392,7 @@ public final class SongTimeline { * null. Must not be used with type == TYPE_SONG or type == TYPE_PLAYLIST * @return The number of songs that were enqueued. */ - public int chooseSongs(boolean enqueue, int type, long id, String selection) + public int addSongs(int mode, int type, long id, String selection) { Cursor cursor; if (type == MediaUtils.TYPE_PLAYLIST) @@ -390,19 +405,31 @@ public final class SongTimeline { if (count == 0) return 0; - Song oldSong = getSong(+1); - ArrayList timeline = mSongs; synchronized (this) { - if (enqueue) { - int i = mCurrentPos + mQueueOffset + 1; - if (i < timeline.size()) - timeline.subList(i, timeline.size()).clear(); - mQueueOffset += count; - } else { - timeline.subList(mCurrentPos + 1, timeline.size()).clear(); - mQueueOffset = count; + saveActiveSongs(); + + switch (mode) { + case MODE_ENQUEUE: { + int i = timeline.size(); + while (--i > mCurrentPos) { + if (timeline.get(i).isRandom()) + timeline.remove(i); + } + break; } + case MODE_PLAY_NEXT: + timeline.subList(mCurrentPos + 1, timeline.size()).clear(); + break; + case MODE_PLAY: + timeline.clear(); + mCurrentPos = 0; + break; + default: + throw new IllegalArgumentException("Invalid mode: " + mode); + } + + int start = timeline.size(); for (int j = 0; j != count; ++j) { cursor.moveToNext(); @@ -412,107 +439,140 @@ public final class SongTimeline { } if (mShuffle) - Collections.shuffle(timeline.subList(timeline.size() - count, timeline.size()), ContextApplication.getRandom()); + Collections.shuffle(timeline.subList(start, timeline.size()), ContextApplication.getRandom()); + + broadcastChangedSongs(); } cursor.close(); - mRepeatedSongs = null; - Song newSong = getSong(+1); - if (newSong != oldSong && mCallback != null) - mCallback.songReplaced(+1, newSong); + changed(); return count; } /** - * Removes any songs greater than 10 songs before the current song (unless - * they are still necessary for repeating). + * Removes any songs greater than 10 songs before the current song when in + * random mode. */ public void purge() { synchronized (this) { - while (mCurrentPos > 10 && mRepeatStart != 0) { - mSongs.remove(0); - --mCurrentPos; - if (mRepeatStart > 0) - --mRepeatStart; + if (mFinishAction == FINISH_RANDOM) { + while (mCurrentPos > 10) { + mSongs.remove(0); + --mCurrentPos; + } } } } - /** - * Finish enqueueing songs for the current session. Newly enqueued songs - * will be enqueued directly after the current song rather than after - * previously enqueued songs. - */ - public void finishEnqueueing() - { - synchronized (this) { - mQueueOffset = 0; - } - } - /** * Clear the song queue. */ public void clearQueue() { synchronized (this) { - mSongs.subList(mCurrentPos + 1, mSongs.size()).clear(); - mQueueOffset = 0; + if (mCurrentPos + 1 < mSongs.size()) + mSongs.subList(mCurrentPos + 1, mSongs.size()).clear(); } - mCallback.songReplaced(+1, getSong(+1)); + mCallback.activeSongReplaced(+1, getSong(+1)); + + changed(); + } + + /** + * Save the active songs for use with broadcastChangedSongs(). + * + * @see SongTimeline#broadcastChangedSongs() + */ + private void saveActiveSongs() + { + mSavedPrevious = getSong(-1); + mSavedCurrent = getSong(0); + mSavedNext = getSong(+1); + } + + /** + * Broadcast the active songs that have changed since the last call to + * saveActiveSongs() + * + * @see SongTimeline#saveActiveSongs() + */ + private void broadcastChangedSongs() + { + Song previous = getSong(-1); + Song current = getSong(0); + Song next = getSong(+1); + + if (mCallback != null) { + if (Song.getId(mSavedPrevious) != Song.getId(previous)) + mCallback.activeSongReplaced(-1, previous); + if (Song.getId(mSavedNext) != Song.getId(next)) + mCallback.activeSongReplaced(1, next); + } + if (Song.getId(mSavedCurrent) != Song.getId(current)) { + if (mCallback != null) + mCallback.activeSongReplaced(0, current); + } } /** * Remove the song with the given id from the timeline. * * @param id The MediaStore id of the song to remove. - * @return True if the current song has changed. */ - public boolean removeSong(long id) + public void removeSong(long id) { synchronized (this) { - boolean changed = false; ArrayList songs = mSongs; - int i = mCurrentPos; - Song oldPrevious = getSong(-1); - Song oldCurrent = getSong(0); - Song oldNext = getSong(+1); + saveActiveSongs(); - while (--i != -1) { + int i = mCurrentPos; + while (--i >= 0) { if (Song.getId(songs.get(i)) == id) { songs.remove(i); --mCurrentPos; } } - for (i = mCurrentPos; i != songs.size(); ++i) { + for (i = mCurrentPos; i < songs.size(); ++i) { if (Song.getId(songs.get(i)) == id) songs.remove(i); } - i = mCurrentPos; - Song previous = getSong(-1); - Song current = getSong(0); - Song next = getSong(+1); + broadcastChangedSongs(); + } - if (mCallback != null) { - if (Song.getId(oldPrevious) != Song.getId(previous)) - mCallback.songReplaced(-1, previous); - if (Song.getId(oldNext) != Song.getId(next)) - mCallback.songReplaced(1, next); - } - if (Song.getId(oldCurrent) != Song.getId(current)) { - if (mCallback != null) - mCallback.songReplaced(0, current); - changed = true; - } + changed(); + } - return changed; + /** + * Broadcasts that the timeline state has changed. + */ + private void changed() + { + if (mCallback != null) + mCallback.timelineChanged(); + } + + /** + * Return true if the finish action is to stop at the end of the queue and + * the current song is the last in the queue. + */ + public boolean isEndOfQueue() + { + synchronized (this) { + return mFinishAction == FINISH_STOP && mCurrentPos == mSongs.size() - 1; + } + } + + public void clear() + { + synchronized (this) { + mSongs.clear(); } } }