Move state file code to PlaybackService and add a version code to state file
This commit is contained in:
parent
48e2aa1d91
commit
5341772d0c
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user