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();
}
}
}