diff --git a/src/org/kreed/vanilla/ContextApplication.java b/src/org/kreed/vanilla/ContextApplication.java index 8f55d75b..372c3185 100644 --- a/src/org/kreed/vanilla/ContextApplication.java +++ b/src/org/kreed/vanilla/ContextApplication.java @@ -121,7 +121,7 @@ public class ContextApplication extends Application { /** * Send a broadcast to all PlaybackActivities that have been added with - * addActivity. + * addActivity and then with Context.sendBroadcast. * * @param intent The intent to be sent as a broadcast */ @@ -136,6 +136,9 @@ public class ContextApplication extends Application { if (activity instanceof PlaybackActivity) ((PlaybackActivity)activity).receive(intent); } + + if (mInstance != null) + mInstance.sendBroadcast(intent); } /** diff --git a/src/org/kreed/vanilla/OneCellWidget.java b/src/org/kreed/vanilla/OneCellWidget.java index a71b3500..670427e7 100644 --- a/src/org/kreed/vanilla/OneCellWidget.java +++ b/src/org/kreed/vanilla/OneCellWidget.java @@ -31,16 +31,14 @@ public class OneCellWidget extends AppWidgetProvider { @Override public void onUpdate(Context context, AppWidgetManager manager, int[] ids) { - PlaybackServiceState state = new PlaybackServiceState(); - Song song = null; - if (state.load(context)) { - song = new Song(state.savedIds[state.savedIndex]); - if (!song.populate(false)) - song = null; - } - - RemoteViews views = createViews(context, song, 0); + SongTimeline timeline = new SongTimeline(); + timeline.loadState(context); + RemoteViews views = createViews(context, timeline.getSong(0), 0); manager.updateAppWidget(ids, views); + + // If we generated a new current song (because the PlaybackService has + // never been started), then we need to save the state. + timeline.saveState(context, 0); } @Override diff --git a/src/org/kreed/vanilla/PlaybackService.java b/src/org/kreed/vanilla/PlaybackService.java index 8ab9d5d9..f035d6ed 100644 --- a/src/org/kreed/vanilla/PlaybackService.java +++ b/src/org/kreed/vanilla/PlaybackService.java @@ -21,9 +21,6 @@ package org.kreed.vanilla; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Random; import android.app.Notification; import android.app.NotificationManager; @@ -54,7 +51,7 @@ import android.util.Log; import android.view.KeyEvent; import android.widget.Toast; -public final class PlaybackService extends Service implements Handler.Callback, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener, SharedPreferences.OnSharedPreferenceChangeListener { +public final class PlaybackService extends Service implements Handler.Callback, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener, SharedPreferences.OnSharedPreferenceChangeListener, SongTimeline.Callback { private static final int NOTIFICATION_ID = 2; private static final int DOUBLE_CLICK_DELAY = 400; @@ -128,9 +125,7 @@ public final class PlaybackService extends Service implements Handler.Callback, private AudioManager mAudioManager; private NotificationManager mNotificationManager; - private ArrayList mSongTimeline; - int mTimelinePos; - private int mQueuePos; + SongTimeline mTimeline; int mState = 0x80; Object mStateLock = new Object(); boolean mPlayingBeforeCall; @@ -143,8 +138,6 @@ public final class PlaybackService extends Service implements Handler.Callback, private boolean mIgnoreNextUp; private boolean mLoaded; boolean mInCall; - private int mRepeatStart = -1; - private ArrayList mRepeatedSongs; private Method mIsWiredHeadsetOn; private Method mStartForeground; @@ -156,19 +149,13 @@ public final class PlaybackService extends Service implements Handler.Callback, HandlerThread thread = new HandlerThread("PlaybackService"); thread.start(); - PlaybackServiceState state = new PlaybackServiceState(); - if (state.load(this)) { - mSongTimeline = new ArrayList(state.savedIds.length); - mTimelinePos = state.savedIndex; - mPendingSeek = state.savedSeek; - mState |= state.savedState; - mRepeatStart = state.repeatStart; - - for (int i = 0; i != state.savedIds.length; ++i) - mSongTimeline.add(new Song(state.savedIds[i], state.savedFlags[i])); - } else { - mSongTimeline = new ArrayList(); - } + mTimeline = new SongTimeline(); + mTimeline.setCallback(this); + mPendingSeek = mTimeline.loadState(this); + if (mTimeline.isRepeating()) + mState |= FLAG_REPEAT; + if (mTimeline.isShuffling()) + mState |= FLAG_SHUFFLE; ContextApplication.setService(this); @@ -179,7 +166,7 @@ public final class PlaybackService extends Service implements Handler.Callback, /** * Show a Toast that notifies the user the Service is starting up. Useful - * to provide a quick reponse to play/pause and next events from widgets + * to provide a quick response to play/pause and next events from widgets * when we must initialize the service before acting on the event. */ private void showStartupToast() @@ -219,11 +206,14 @@ public final class PlaybackService extends Service implements Handler.Callback, sendOrderedBroadcast(intent, null); } } else if (ACTION_PLAY_ITEMS.equals(action)) { - chooseSongs(false, intent.getIntExtra("type", 3), intent.getLongExtra("id", -1)); + mTimeline.chooseSongs(false, intent.getIntExtra("type", 3), intent.getLongExtra("id", -1)); + mHandler.sendEmptyMessage(TRACK_CHANGED); } else if (ACTION_ENQUEUE_ITEMS.equals(action)) { - chooseSongs(true, intent.getIntExtra("type", 3), intent.getLongExtra("id", -1)); + mTimeline.chooseSongs(true, intent.getIntExtra("type", 3), intent.getLongExtra("id", -1)); + mHandler.removeMessages(SAVE_STATE); + mHandler.sendEmptyMessageDelayed(SAVE_STATE, 5000); } else if (ACTION_FINISH_ENQUEUEING.equals(action)) { - mQueuePos = 0; + mTimeline.finishEnqueueing(); } if (delta != -10) { @@ -242,10 +232,9 @@ public final class PlaybackService extends Service implements Handler.Callback, super.onDestroy(); - if (mSongTimeline != null) - saveState(true); - if (mMediaPlayer != null) { + mTimeline.saveState(this, mMediaPlayer.getCurrentPosition()); + unsetFlag(FLAG_PLAYING); mMediaPlayer.release(); mMediaPlayer = null; @@ -259,7 +248,7 @@ public final class PlaybackService extends Service implements Handler.Callback, // we haven't registered the receiver yet } - // Renable the external receiver + // Re-enable the external receiver PackageManager manager = getPackageManager(); manager.setComponentEnabledSetting(new ComponentName(this, MediaButtonReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, PackageManager.DONT_KILL_APP); @@ -303,7 +292,7 @@ public final class PlaybackService extends Service implements Handler.Callback, private void initialize() { - sendBroadcast(new Intent(EVENT_INITIALIZED)); + ContextApplication.broadcast(new Intent(EVENT_INITIALIZED)); mMediaPlayer = new MediaPlayer(); mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); @@ -381,24 +370,12 @@ public final class PlaybackService extends Service implements Handler.Callback, } } - @Override - public void sendBroadcast(Intent intent) - { - ContextApplication.broadcast(intent); - super.sendBroadcast(intent); - } - private void broadcastReplaceSong(int delta, Song song) { Intent intent = new Intent(EVENT_REPLACE_SONG); intent.putExtra("pos", delta); intent.putExtra("song", song); - sendBroadcast(intent); - } - - private void broadcastReplaceSong(int delta) - { - broadcastReplaceSong(delta, getSong(delta)); + ContextApplication.broadcast(intent); } boolean setFlag(int flag) @@ -433,8 +410,8 @@ public final class PlaybackService extends Service implements Handler.Callback, Intent intent = new Intent(EVENT_CHANGED); intent.putExtra("state", state); intent.putExtra("song", song); - intent.putExtra("pos", mTimelinePos); - sendBroadcast(intent); + intent.putExtra("pos", mTimeline.getCurrentPosition()); + ContextApplication.broadcast(intent); if (mScrobble) { intent = new Intent("net.jjc1138.android.scrobbler.action.MUSIC_STATUS"); @@ -449,15 +426,8 @@ public final class PlaybackService extends Service implements Handler.Callback, mLastSongBroadcast = song; } - // The current song is the starting point for repeated tracks - if ((state & FLAG_REPEAT) != 0 && (oldState & FLAG_REPEAT) == 0) { - song.flags &= ~Song.FLAG_RANDOM; - mRepeatStart = mTimelinePos; - broadcastReplaceSong(+1); - } else if ((state & FLAG_REPEAT) == 0 && (oldState & FLAG_REPEAT) != 0) { - mRepeatStart = -1; - broadcastReplaceSong(+1); - } + mTimeline.setRepeat((state & FLAG_REPEAT) != 0); + mTimeline.setShuffle((state & FLAG_SHUFFLE) != 0); if ((state & FLAG_NO_MEDIA) != 0 && (oldState & FLAG_NO_MEDIA) == 0) { ContentResolver resolver = ContextApplication.getContext().getContentResolver(); @@ -540,17 +510,6 @@ public final class PlaybackService extends Service implements Handler.Callback, } } - private ArrayList getShuffledRepeatedSongs(int end) - { - ArrayList songs = mRepeatedSongs; - if (songs == null) { - songs = new ArrayList(mSongTimeline.subList(mRepeatStart, end)); - Collections.shuffle(songs, ContextApplication.getRandom()); - mRepeatedSongs = songs; - } - return songs; - } - /** * Move delta places away from the current song. */ @@ -559,34 +518,14 @@ public final class PlaybackService extends Service implements Handler.Callback, if (mMediaPlayer == null) return; - Song song = getSong(delta); + Song song = mTimeline.shiftCurrentSong(delta); if (song == null) { setFlag(FLAG_NO_MEDIA); return; - } else { + } else if ((mState & FLAG_NO_MEDIA) != 0) { unsetFlag(FLAG_NO_MEDIA); } - ArrayList timeline = mSongTimeline; - synchronized (timeline) { - if (delta == 1 && mRepeatStart >= 0 && (timeline.get(mTimelinePos + 1).flags & Song.FLAG_RANDOM) != 0) { - if ((mState & FLAG_SHUFFLE) == 0) { - mTimelinePos = mRepeatStart; - } else { - int j = mTimelinePos + delta; - ArrayList songs = getShuffledRepeatedSongs(j); - for (int i = songs.size(); --i != -1 && --j != -1; ) - mSongTimeline.set(j, songs.get(i)); - mRepeatedSongs = null; - mTimelinePos = j; - } - song = getSong(0); - broadcastReplaceSong(-1); - } else { - mTimelinePos += delta; - } - } - try { synchronized (mMediaPlayer) { mMediaPlayer.reset(); @@ -597,6 +536,9 @@ public final class PlaybackService extends Service implements Handler.Callback, } if ((mState & FLAG_PLAYING) != 0) mMediaPlayer.start(); + // Ensure that we broadcast a change event even if we play the same + // song again. + mLastSongBroadcast = null; updateState(mState); } catch (IOException e) { Log.e("VanillaMusic", "IOException", e); @@ -626,115 +568,15 @@ public final class PlaybackService extends Service implements Handler.Callback, } /** - * Returns the song delta places away from the current position. + * Returns the song delta places away from the current + * position. */ public Song getSong(int delta) { - if (mSongTimeline == null) + if (mTimeline == null) return null; - ArrayList timeline = mSongTimeline; - Song song; - - synchronized (timeline) { - int pos = mTimelinePos + delta; - if (pos < 0) - return null; - - int size = timeline.size(); - if (pos > size) - return null; - - if (pos == size) { - song = new Song(); - timeline.add(song); - } else { - song = timeline.get(pos); - } - - if (delta == 1 && mRepeatStart >= 0 && (song.flags & Song.FLAG_RANDOM) != 0) { - // We have reached a non-user-selected song; this song will - // repeated in setCurrentSong so take alternative measures - if ((mState & FLAG_SHUFFLE) == 0) - song = timeline.get(mRepeatStart); - else - song = getShuffledRepeatedSongs(mTimelinePos + delta).get(0); - } - } - - if (!song.populate(false)) { - song.randomize(); - if (!song.populate(false)) - return null; - } - - return song; - } - - /** - * Add a set of songs to the song timeline. There are two modes: play and - * enqueue. Play will play the first song in the set immediately and enqueue - * the remaining songs directly after it. Enqueue will place the set after - * the last enqueued item or after the currently playing item if the queue - * is empty. - * - * @param enqueue If true, enqueue the set. If false, play the set. - * @param type 1, 2, or 3, indicating artist, album, or song, respectively. - * @param id The MediaStore id of the artist, album, or song. - */ - private void chooseSongs(boolean enqueue, int type, long id) - { - long[] songs = Song.getAllSongIdsWith(type, id); - if (songs == null || songs.length == 0) - return; - - if ((mState & FLAG_SHUFFLE) != 0) { - Random random = ContextApplication.getRandom(); - for (int i = songs.length; --i != 0; ) { - int j = random.nextInt(i + 1); - long tmp = songs[j]; - songs[j] = songs[i]; - songs[i] = tmp; - } - } - - Song oldSong = getSong(+1); - - ArrayList timeline = mSongTimeline; - synchronized (timeline) { - if (enqueue) { - int i = mTimelinePos + mQueuePos + 1; - if (i < timeline.size()) - timeline.subList(i, timeline.size()).clear(); - - for (int j = 0; j != songs.length; ++j) - timeline.add(new Song(songs[j])); - - mQueuePos += songs.length; - } else { - timeline.subList(mTimelinePos + 1, timeline.size()).clear(); - - for (int j = 0; j != songs.length; ++j) - timeline.add(new Song(songs[j])); - - mQueuePos += songs.length - 1; - - mHandler.sendEmptyMessage(TRACK_CHANGED); - } - } - - mRepeatedSongs = null; - Song newSong = getSong(+1); - if (newSong != oldSong) - broadcastReplaceSong(+1, newSong); - - mHandler.removeMessages(SAVE_STATE); - mHandler.sendEmptyMessageDelayed(SAVE_STATE, 5000); - } - - private void saveState(boolean savePosition) - { - PlaybackServiceState.saveState(this, mSongTimeline, mTimelinePos, savePosition && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0, mState & (FLAG_REPEAT + FLAG_SHUFFLE), mRepeatStart); + return mTimeline.getSong(delta); } private void go(int delta, boolean autoPlay) @@ -925,19 +767,11 @@ public final class PlaybackService extends Service implements Handler.Callback, case SAVE_STATE: // For unexpected terminations: crashes, task killers, etc. // In most cases onDestroy will handle this - saveState(false); + mTimeline.saveState(this, 0); break; case PROCESS_SONG: getSong(+2); - - synchronized (mSongTimeline) { - while (mTimelinePos > 15 && mRepeatStart > 0) { - mSongTimeline.remove(0); - --mTimelinePos; - --mRepeatStart; - } - } - + mTimeline.purge(); mHandler.removeMessages(SAVE_STATE); mHandler.sendEmptyMessageDelayed(SAVE_STATE, 5000); break; @@ -1004,7 +838,7 @@ public final class PlaybackService extends Service implements Handler.Callback, */ public int getTimelinePos() { - return mTimelinePos; + return mTimeline.getCurrentPosition(); } /** @@ -1027,4 +861,12 @@ 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) + { + broadcastReplaceSong(delta, song); + } } diff --git a/src/org/kreed/vanilla/PlaybackServiceState.java b/src/org/kreed/vanilla/PlaybackServiceState.java deleted file mode 100644 index 99580839..00000000 --- a/src/org/kreed/vanilla/PlaybackServiceState.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2010 Christopher Eby - * - * This file is part of Vanilla Music Player. - * - * Vanilla Music Player is free software; you can redistribute it and/or modify - * it under the terms of the GNU Library General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Vanilla Music Player is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.kreed.vanilla; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.List; - -import android.content.Context; -import android.util.Log; - -public class PlaybackServiceState { - private static final String STATE_FILE = "state"; - private static final long STATE_FILE_MAGIC = 0x8a9d3f2fca32L; - - public int savedIndex; - public long[] savedIds; - public int[] savedFlags; - public int savedSeek; - public int savedState; - public int repeatStart; - - public boolean load(Context context) - { - try { - DataInputStream in = new DataInputStream(context.openFileInput(STATE_FILE)); - if (in.readLong() == STATE_FILE_MAGIC) { - savedIndex = in.readInt(); - int n = in.readInt(); - - if (n > 0) { - savedIds = new long[n]; - savedFlags = new int[n]; - for (int i = 0; i != n; ++i) { - savedIds[i] = in.readLong(); - savedFlags[i] = in.readInt(); - } - savedSeek = in.readInt(); - savedState = in.readInt(); - repeatStart = in.readInt(); - } - - in.close(); - return n > 0; - } - } catch (FileNotFoundException e) { - } catch (IOException e) { - Log.w("VanillaMusic", e); - } - - return false; - } - - public static void saveState(Context context, List songs, int index, int seek, int state, int repeatStart) - { - try { - DataOutputStream out = new DataOutputStream(context.openFileOutput(STATE_FILE, 0)); - out.writeLong(STATE_FILE_MAGIC); - out.writeInt(index); - int n = songs == null ? 0 : songs.size(); - out.writeInt(n); - for (int i = 0; i != n; ++i) { - Song song = songs.get(i); - out.writeLong(song.id); - out.writeInt(song.flags); - } - out.writeInt(seek); - out.writeInt(state); - out.writeInt(repeatStart); - out.close(); - } catch (IOException e) { - Log.w("VanillaMusic", e); - } - } -} diff --git a/src/org/kreed/vanilla/SongTimeline.java b/src/org/kreed/vanilla/SongTimeline.java new file mode 100644 index 00000000..171c26d3 --- /dev/null +++ b/src/org/kreed/vanilla/SongTimeline.java @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2010 Christopher Eby + * + * This file is part of Vanilla Music Player. + * + * Vanilla Music Player is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Vanilla Music Player is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kreed.vanilla; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Random; + +import android.content.Context; +import android.util.Log; + +/** + * Represents a series of songs that can be moved through backward or forward. + * Automatically handles the fetching of new (random) songs when a song does not + * exist at a requested position. + */ +public final class SongTimeline { + /** + * Name of the state file. + */ + private static final String STATE_FILE = "state"; + /** + * Header for state file to help indicate if the file is in the right + * format. + */ + private static final long STATE_FILE_MAGIC = 0x8a9d3f2fca33L; + + /** + * All the songs currently contained in the timeline. Each Song object + * should be unique, even if it refers to the same media. + */ + private ArrayList mSongs = new ArrayList(); + /** + * 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; + + public interface Callback { + /** + * Called when a song in the timeline has been replaced with a + * different song. + * + * @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); + } + /** + * The current Callback, if any. + */ + private Callback mCallback; + + /** + * 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 + */ + public int loadState(Context context) + { + int extra = -1; + + try { + DataInputStream in = new DataInputStream(context.openFileInput(STATE_FILE)); + if (in.readLong() == STATE_FILE_MAGIC) { + 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(); + mShuffle = in.readBoolean(); + extra = in.readInt(); + } + } + + in.close(); + } catch (IOException e) { + Log.w("VanillaMusic", "Failed to load state", e); + } + + return extra; + } + + /** + * Returns a byte array representing the current state of the timeline. + * This can be passed to the appropriate constructor to initialize the + * timeline with this state. + * + * @param context The Context to open the state file with + * @param extra Optional extra data to be included. Should not be -1. + */ + public void saveState(Context context, int extra) + { + try { + DataOutputStream out = new DataOutputStream(context.openFileOutput(STATE_FILE, 0)); + out.writeLong(STATE_FILE_MAGIC); + + synchronized (this) { + ArrayList songs = mSongs; + + int size = songs.size(); + out.writeInt(size); + + for (int i = 0; i != size; ++i) { + Song song = songs.get(i); + out.writeLong(song.id); + out.writeInt(song.flags); + } + + out.writeInt(mCurrentPos); + out.writeInt(mRepeatStart); + out.writeBoolean(mShuffle); + out.writeInt(extra); + } + + out.close(); + } catch (IOException e) { + Log.w("VanillaMusic", "Failed to save state", e); + } + } + + /** + * Sets the current callback to callback. + */ + public void setCallback(Callback callback) + { + mCallback = callback; + } + + /** + * Return whether shuffling is enabled. + */ + public boolean isShuffling() + { + return mShuffle; + } + + /** + * Return whether repeating is enabled. + */ + public boolean isRepeating() + { + return mRepeatStart != -1; + } + + /** + * Return the position of the current song (i.e. the playing song). + */ + public int getCurrentPosition() + { + synchronized (this) { + return mCurrentPos; + } + } + + /** + * Set whether shuffling is enabled. Shuffling will shuffle sets of songs + * that are added with chooseSongs and shuffle sets of repeated songs. + */ + public void setShuffle(boolean shuffle) + { + mShuffle = shuffle; + } + + /** + * 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). + */ + public void setRepeat(boolean repeat) + { + // Don't change anything if we are already doing what we want. + if (repeat == (mRepeatStart != -1)) + return; + + synchronized (this) { + if (repeat) { + mRepeatStart = mCurrentPos; + // Ensure that we will at least repeat one song (the current song), + // even if all of our songs were selected randomly. + getSong(0).flags &= ~Song.FLAG_RANDOM; + } else { + mRepeatStart = -1; + mRepeatedSongs = null; + } + + if (mCallback != null) + mCallback.songReplaced(+1, getSong(+1)); + } + } + + /** + * Retrieves a shuffled list of the songs to be repeated. This caches the + * results so that the repeated songs are shuffled only once. + * + * @param end The position just after the last song to be included in the + * repeated songs + */ + private ArrayList getShuffledRepeatedSongs(int end) + { + if (mRepeatedSongs == null) { + ArrayList songs = new ArrayList(mSongs.subList(mRepeatStart, end)); + Collections.shuffle(songs, ContextApplication.getRandom()); + mRepeatedSongs = songs; + } + return mRepeatedSongs; + } + + /** + * Returns the song delta places away from the current + * position. + */ + public Song getSong(int delta) + { + ArrayList timeline = mSongs; + Song song; + + synchronized (this) { + int pos = mCurrentPos + delta; + if (pos < 0) + return null; + + int size = timeline.size(); + if (pos > size) + return null; + + if (pos == size) { + song = new Song(); + timeline.add(song); + } else { + song = timeline.get(pos); + } + + if (mRepeatStart != -1 && (song.flags & Song.FLAG_RANDOM) != 0) { + if (delta == 1) { + // We have reached a non-user-selected song; this song will + // repeated in shiftCurrentSong so take alternative + // measures + if (mShuffle) + song = getShuffledRepeatedSongs(mCurrentPos + delta).get(0); + 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)); + } + } + } + + if (!song.populate(false)) { + song.randomize(); + if (!song.populate(false)) + return null; + } + + return song; + } + + /** + * Shift the current song by delta places. + * + * @return The Song at the new position + */ + public Song shiftCurrentSong(int delta) + { + synchronized (this) { + mCurrentPos += delta; + return getSong(0); + } + } + + /** + * Add a set of songs to the song timeline. There are two modes: play and + * enqueue. Play will play the first song in the set immediately and enqueue + * the remaining songs directly after it. Enqueue will place the set after + * the last enqueued item or after the currently playing item if the queue + * is empty. + * + * @param enqueue If true, enqueue the set. If false, play the set. + * @param type 1, 2, or 3, indicating artist, album, or song, respectively. + * @param id The MediaStore id of the artist, album, or song. + */ + public void chooseSongs(boolean enqueue, int type, long id) + { + long[] songs = Song.getAllSongIdsWith(type, id); + if (songs == null || songs.length == 0) + return; + + if (mShuffle) { + Random random = ContextApplication.getRandom(); + for (int i = songs.length; --i != 0; ) { + int j = random.nextInt(i + 1); + long tmp = songs[j]; + songs[j] = songs[i]; + songs[i] = tmp; + } + } + + 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(); + + for (int j = 0; j != songs.length; ++j) + timeline.add(new Song(songs[j])); + + mQueueOffset += songs.length; + } else { + timeline.subList(mCurrentPos + 1, timeline.size()).clear(); + + for (int j = 0; j != songs.length; ++j) + timeline.add(new Song(songs[j])); + + mQueueOffset += songs.length - 1; + } + } + + mRepeatedSongs = null; + Song newSong = getSong(+1); + if (newSong != oldSong && mCallback != null) + mCallback.songReplaced(+1, newSong); + } + + /** + * Removes any songs greater than 10 songs before the current song (unless + * they are still necessary for repeating). + */ + public void purge() + { + synchronized (this) { + while (mCurrentPos > 10 && mRepeatStart > 0) { + mSongs.remove(0); + --mCurrentPos; + --mRepeatStart; + } + } + } + + /** + * 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; + } + } +}