Add random option
Disabling this option makes Vanilla function more like a typical music player.
This commit is contained in:
parent
0f95ac6336
commit
5c45c4540a
@ -50,6 +50,7 @@ THE SOFTWARE.
|
||||
<item>Previous Song</item>
|
||||
<item>Toggle Repeat</item>
|
||||
<item>Toggle Shuffle</item>
|
||||
<item>Toggle Random</item>
|
||||
<item>Enqueue Current Album</item>
|
||||
<item>Enqueue Current Artist</item>
|
||||
<item>Enqueue Current Genre</item>
|
||||
@ -70,5 +71,6 @@ THE SOFTWARE.
|
||||
<item>8</item>
|
||||
<item>9</item>
|
||||
<item>10</item>
|
||||
<item>11</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
@ -25,18 +25,23 @@ THE SOFTWARE.
|
||||
|
||||
<!-- Playback Activity (main screen) -->
|
||||
<string name="no_songs">No songs found on your device.</string>
|
||||
<string name="empty_queue">No songs selected. Pick some from the library (search key) or enter random mode (in the menu).</string>
|
||||
<string name="settings">Preferences</string>
|
||||
<string name="library">Library</string>
|
||||
<string name="quit">Stop and Exit</string>
|
||||
<string name="display_mode">Display Mode</string>
|
||||
<string name="shuffle_enable">Enable Shuffle</string>
|
||||
<string name="shuffle_disable">Disable Shuffle</string>
|
||||
<string name="shuffle_enabling">Shuffle enabled for newly added songs</string>
|
||||
<string name="shuffle_enabling">Shuffle enabled</string>
|
||||
<string name="shuffle_disabling">Shuffle disabled</string>
|
||||
<string name="repeat_enable">Enable Repeat</string>
|
||||
<string name="repeat_disable">Disable Repeat</string>
|
||||
<string name="repeat_enabling">Repeat enabled. The current song and any songs you have added after it will be repeated.</string>
|
||||
<string name="repeat_enabling">Repeat enabled</string>
|
||||
<string name="repeat_disabling">Repeat disabled</string>
|
||||
<string name="random_enable">Enable Random</string>
|
||||
<string name="random_disable">Disable Random</string>
|
||||
<string name="random_enabling">Random enabled</string>
|
||||
<string name="random_disabling">Random disabled</string>
|
||||
<string name="song_load_failed">Failed to load song %s. It may be corrupt or missing.</string>
|
||||
<string name="queue_cleared">Queue cleared.</string>
|
||||
|
||||
|
@ -76,7 +76,6 @@ public final class CoverView extends View implements Handler.Callback {
|
||||
*/
|
||||
private Cache<Bitmap> mBitmapCache = new Cache<Bitmap>(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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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<Song> 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<Song> 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<Song> songs = new ArrayList<Song>(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<Song> getShuffledRepeatedSongs(int end)
|
||||
private Song shuffleAll()
|
||||
{
|
||||
if (mRepeatedSongs == null) {
|
||||
ArrayList<Song> songs = new ArrayList<Song>(mSongs.subList(mRepeatStart, end));
|
||||
Collections.shuffle(songs, ContextApplication.getRandom());
|
||||
mRepeatedSongs = songs;
|
||||
}
|
||||
return mRepeatedSongs;
|
||||
ArrayList<Song> songs = new ArrayList<Song>(mSongs);
|
||||
Collections.shuffle(songs, ContextApplication.getRandom());
|
||||
mShuffledSongs = songs;
|
||||
return songs.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the song <code>delta</code> 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<Song> 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<Song> 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 <code>delta</code> 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<Song> 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<Song> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user