Move state file code to PlaybackService and add a version code to state file

This commit is contained in:
Christopher Eby 2011-09-21 17:32:38 -05:00
parent 48e2aa1d91
commit 5341772d0c
2 changed files with 148 additions and 124 deletions

View File

@ -22,6 +22,9 @@
package org.kreed.vanilla; package org.kreed.vanilla;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -50,6 +53,20 @@ import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
public final class PlaybackService extends Service implements Handler.Callback, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener, SharedPreferences.OnSharedPreferenceChangeListener, SongTimeline.Callback { public final class PlaybackService extends Service implements Handler.Callback, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener, SharedPreferences.OnSharedPreferenceChangeListener, SongTimeline.Callback {
/**
* 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 = 0x1533574DC74B6ECL;
/**
* State file version that indicates data order.
*/
private static final int STATE_VERSION = 1;
private static final int NOTIFICATION_ID = 2; private static final int NOTIFICATION_ID = 2;
/** /**
@ -186,7 +203,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
mTimeline = new SongTimeline(this); mTimeline = new SongTimeline(this);
mTimeline.setCallback(this); mTimeline.setCallback(this);
mPendingSeek = mTimeline.loadState(); int state = loadState();
mMediaPlayer = new MediaPlayer(); mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
@ -218,14 +235,6 @@ public final class PlaybackService extends Service implements Handler.Callback,
initWidgets(); initWidgets();
int state = 0;
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;
updateState(state); updateState(state);
setCurrentSong(0); setCurrentSong(0);
@ -285,7 +294,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
stopForeground(true); stopForeground(true);
if (mMediaPlayer != null) { if (mMediaPlayer != null) {
mTimeline.saveState(mMediaPlayer.getCurrentPosition()); saveState(mMediaPlayer.getCurrentPosition());
mMediaPlayer.release(); mMediaPlayer.release();
mMediaPlayer = null; mMediaPlayer = null;
} }
@ -826,7 +835,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
case SAVE_STATE: case SAVE_STATE:
// For unexpected terminations: crashes, task killers, etc. // For unexpected terminations: crashes, task killers, etc.
// In most cases onDestroy will handle this // In most cases onDestroy will handle this
mTimeline.saveState(0); saveState(0);
break; break;
case PROCESS_SONG: case PROCESS_SONG:
processSong((Song)message.obj); processSong((Song)message.obj);
@ -1135,4 +1144,60 @@ public final class PlaybackService extends Service implements Handler.Callback,
{ {
sActivities.remove(activity); sActivities.remove(activity);
} }
/**
* Initializes the service state, loading songs saved from the disk into the
* song timeline.
*
* @return The loaded value for mState.
*/
public int loadState()
{
int state = 0;
try {
DataInputStream in = new DataInputStream(openFileInput(STATE_FILE));
if (in.readLong() == STATE_FILE_MAGIC && in.readInt() == STATE_VERSION) {
mPendingSeek = in.readInt();
mTimeline.readState(in);
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;
}
in.close();
} catch (EOFException e) {
Log.w("VanillaMusic", "Failed to load state", e);
} catch (IOException e) {
Log.w("VanillaMusic", "Failed to load state", e);
}
return state;
}
/**
* Save the service state to disk.
*
* @param pendingSeek The pendingSeek to store. Should be the current
* MediaPlayer position or 0.
*/
public void saveState(int pendingSeek)
{
try {
DataOutputStream out = new DataOutputStream(openFileOutput(STATE_FILE, 0));
out.writeLong(STATE_FILE_MAGIC);
out.writeInt(STATE_VERSION);
out.writeInt(pendingSeek);
mTimeline.writeState(out);
out.close();
} catch (IOException e) {
Log.w("VanillaMusic", "Failed to save state", e);
}
}
} }

View File

@ -36,7 +36,6 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.util.Log;
/** /**
* Represents a series of songs that can be moved through backward or forward. * Represents a series of songs that can be moved through backward or forward.
@ -82,23 +81,12 @@ public final class SongTimeline {
*/ */
public static final int MODE_ENQUEUE = 2; public static final int MODE_ENQUEUE = 2;
/**
* 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 = 0xf89daa2fac33L;
private Context mContext; private Context mContext;
/** /**
* All the songs currently contained in the timeline. Each Song object * All the songs currently contained in the timeline. Each Song object
* should be unique, even if it refers to the same media. * should be unique, even if it refers to the same media.
*/ */
private ArrayList<Song> mSongs; private ArrayList<Song> mSongs = new ArrayList<Song>();
/** /**
* The position of the current song (i.e. the playing song). * The position of the current song (i.e. the playing song).
*/ */
@ -176,133 +164,104 @@ public final class SongTimeline {
} }
/** /**
* Initializes the timeline with the state stored in the state file created * Initializes the timeline with data read from the stream. Data should have
* by a call to save state. * been saved by a call to {@link SongTimeline#writeState(DataOutputStream)}.
* *
* @return The optional extra data, or -1 if loading failed * @param in The stream to read from.
*/ */
public int loadState() public void readState(DataInputStream in) throws IOException, EOFException
{ {
int extra = -1; synchronized (this) {
int n = in.readInt();
if (n > 0) {
ArrayList<Song> songs = new ArrayList<Song>(n);
try { // Fill the selection with the ids of all the saved songs
synchronized (this) { // and initialize the timeline with unpopulated songs.
DataInputStream in = new DataInputStream(mContext.openFileInput(STATE_FILE)); StringBuilder selection = new StringBuilder("_ID IN (");
if (in.readLong() == STATE_FILE_MAGIC) { for (int i = 0; i != n; ++i) {
int n = in.readInt(); long id = in.readLong();
if (n > 0) { int flags = in.readInt();
ArrayList<Song> songs = new ArrayList<Song>(n); // Add the index so we can sort
flags |= i << Song.FLAG_COUNT;
songs.add(new Song(id, flags));
// Fill the selection with the ids of all the saved songs if (i != 0)
// and initialize the timeline with unpopulated songs. selection.append(',');
StringBuilder selection = new StringBuilder("_ID IN ("); selection.append(id);
for (int i = 0; i != n; ++i) { }
long id = in.readLong(); selection.append(')');
int flags = in.readInt();
// Add the index so we can sort
flags |= i << Song.FLAG_COUNT;
songs.add(new Song(id, flags));
if (i != 0) // Sort songs by id---this is the order the query will
selection.append(','); // return its results in.
selection.append(id); Collections.sort(songs, new IdComparator());
}
selection.append(')');
// Sort songs by id---this is the order the query will ContentResolver resolver = mContext.getContentResolver();
// return its results in. Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
Collections.sort(songs, new IdComparator());
ContentResolver resolver = mContext.getContentResolver(); Cursor cursor = resolver.query(media, Song.FILLED_PROJECTION, selection.toString(), null, "_id");
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; if (cursor != null) {
cursor.moveToNext();
Cursor cursor = resolver.query(media, Song.FILLED_PROJECTION, selection.toString(), null, "_id"); // Loop through timeline entries, looking for a row
if (cursor != null) { // that matches the id. One row may match multiple
// entries.
Iterator<Song> it = songs.iterator();
while (it.hasNext()) {
Song e = it.next();
while (cursor.getLong(0) < e.id)
cursor.moveToNext(); cursor.moveToNext();
if (cursor.getLong(0) == e.id)
// Loop through timeline entries, looking for a row e.populate(cursor);
// that matches the id. One row may match multiple else
// entries. // We weren't able to query this song.
Iterator<Song> it = songs.iterator(); it.remove();
while (it.hasNext()) {
Song e = it.next();
while (cursor.getLong(0) < e.id)
cursor.moveToNext();
if (cursor.getLong(0) == e.id)
e.populate(cursor);
else
// We weren't able to query this song.
it.remove();
}
cursor.close();
// Revert to the order the songs were saved in.
Collections.sort(songs, new FlagComparator());
mSongs = songs;
}
} }
mCurrentPos = Math.min(mSongs == null ? 0 : mSongs.size(), in.readInt()); cursor.close();
mFinishAction = in.readInt();
mShuffle = in.readBoolean();
extra = in.readInt();
in.close(); // Revert to the order the songs were saved in.
Collections.sort(songs, new FlagComparator());
mSongs = songs;
} }
} }
} catch (EOFException e) {
Log.w("VanillaMusic", "Failed to load state", e); mCurrentPos = Math.min(mSongs == null ? 0 : mSongs.size(), in.readInt());
} catch (IOException e) { mFinishAction = in.readInt();
Log.w("VanillaMusic", "Failed to load state", e); mShuffle = in.readBoolean();
} }
if (mSongs == null)
mSongs = new ArrayList<Song>();
return extra;
} }
/** /**
* Returns a byte array representing the current state of the timeline. * Writes the current songs and state to the given stream.
* This can be passed to the appropriate constructor to initialize the
* timeline with this state.
* *
* @param extra Optional extra data to be included. Should not be -1. * @param out The stream to write to.
*/ */
public void saveState(int extra) public void writeState(DataOutputStream out) throws IOException
{ {
try { // Must update PlaybackService.STATE_VERSION when changing behavior
DataOutputStream out = new DataOutputStream(mContext.openFileOutput(STATE_FILE, 0)); // here.
out.writeLong(STATE_FILE_MAGIC); synchronized (this) {
ArrayList<Song> songs = mSongs;
synchronized (this) { int size = songs.size();
ArrayList<Song> songs = mSongs; out.writeInt(size);
int size = songs.size(); for (int i = 0; i != size; ++i) {
out.writeInt(size); Song song = songs.get(i);
if (song == null) {
for (int i = 0; i != size; ++i) { out.writeLong(-1);
Song song = songs.get(i); out.writeInt(0);
if (song == null) { } else {
out.writeLong(-1); out.writeLong(song.id);
out.writeInt(0); out.writeInt(song.flags);
} else {
out.writeLong(song.id);
out.writeInt(song.flags);
}
} }
out.writeInt(mCurrentPos);
out.writeInt(mFinishAction);
out.writeBoolean(mShuffle);
out.writeInt(extra);
} }
out.close(); out.writeInt(mCurrentPos);
} catch (IOException e) { out.writeInt(mFinishAction);
Log.w("VanillaMusic", "Failed to save state", e); out.writeBoolean(mShuffle);
} }
} }