Split out song timeline code
This commit is contained in:
parent
daafda3c75
commit
4de4f8a31e
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
417
src/org/kreed/vanilla/SongTimeline.java
Normal file
417
src/org/kreed/vanilla/SongTimeline.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user