Split out song timeline code

This commit is contained in:
Christopher Eby 2010-05-09 20:57:38 -05:00
parent daafda3c75
commit 4de4f8a31e
5 changed files with 473 additions and 306 deletions

View File

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

View File

@ -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

View File

@ -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<Song> 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<Song> 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<Song>(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<Song>();
}
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<Song> getShuffledRepeatedSongs(int end)
{
ArrayList<Song> songs = mRepeatedSongs;
if (songs == null) {
songs = new ArrayList<Song>(mSongTimeline.subList(mRepeatStart, end));
Collections.shuffle(songs, ContextApplication.getRandom());
mRepeatedSongs = songs;
}
return songs;
}
/**
* Move <code>delta</code> 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<Song> 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<Song> 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 <code>delta</code> places away from the current position.
* Returns the song <code>delta</code> places away from the current
* position.
*/
public Song getSong(int delta)
{
if (mSongTimeline == null)
if (mTimeline == null)
return null;
ArrayList<Song> 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<Song> 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);
}
}

View File

@ -1,93 +0,0 @@
/*
* Copyright (C) 2010 Christopher Eby <kreed@kreed.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Song> 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);
}
}
}

View File

@ -0,0 +1,417 @@
/*
* Copyright (C) 2010 Christopher Eby <kreed@kreed.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Song> mSongs = new ArrayList<Song>();
/**
* 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;
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<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();
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<Song> 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 <code>callback</code>.
*/
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<Song> getShuffledRepeatedSongs(int end)
{
if (mRepeatedSongs == null) {
ArrayList<Song> songs = new ArrayList<Song>(mSongs.subList(mRepeatStart, end));
Collections.shuffle(songs, ContextApplication.getRandom());
mRepeatedSongs = songs;
}
return mRepeatedSongs;
}
/**
* Returns the song <code>delta</code> places away from the current
* position.
*/
public Song getSong(int delta)
{
ArrayList<Song> 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<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));
}
}
}
if (!song.populate(false)) {
song.randomize();
if (!song.populate(false))
return null;
}
return song;
}
/**
* Shift the current song by <code>delta</code> 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<Song> 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;
}
}
}