diff --git a/res/values/strings.xml b/res/values/strings.xml
index f2114dc5..70698eea 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -31,8 +31,10 @@ THE SOFTWARE.
Display Mode
Enable Shuffle
Disable Shuffle
- Shuffle enabled
- Shuffle disabled
+ Shuffle Albums
+ Song shuffle enabled
+ Album shuffle enabled
+ Shuffle disabled
Enable Repeat
Repeat Song
Disable Repeat
diff --git a/src/org/kreed/vanilla/MediaUtils.java b/src/org/kreed/vanilla/MediaUtils.java
index babe3c23..f57cc6c5 100644
--- a/src/org/kreed/vanilla/MediaUtils.java
+++ b/src/org/kreed/vanilla/MediaUtils.java
@@ -22,14 +22,16 @@
package org.kreed.vanilla;
-import java.io.File;
-import java.util.Random;
-
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
public class MediaUtils {
/**
@@ -294,6 +296,87 @@ public class MediaUtils {
}
}
+ /**
+ * Shuffle a Song list using Fisher-Yates algorithm.
+ *
+ * @param albumShuffle If true, preserve the order of tracks inside albums.
+ */
+ public static void shuffle(List list, boolean albumShuffle)
+ {
+ int size = list.size();
+ if (size < 2)
+ return;
+
+ Random random = getRandom();
+ if (albumShuffle) {
+ Song[] songs = list.toArray(new Song[size]);
+ Song[] temp = new Song[size];
+
+ // Make sure the albums are in order
+ Arrays.sort(songs);
+
+ // This is Fisher-Yates algorithm, but it swaps albums instead of
+ // single elements.
+ for (int i = size; --i != -1; ) {
+ Song songI = songs[i];
+ if (i > 0 && songs[i - 1].albumId == songI.albumId)
+ // This index is not the start of an album. Skip it.
+ continue;
+
+ int j = random.nextInt(i + 1);
+ while (j > 0 && songs[j - 1].albumId == songs[j].albumId)
+ // This index is not the start of an album. Find the start.
+ j -= 1;
+
+ int lowerStart = Math.min(i, j);
+ int upperStart = Math.max(i, j);
+
+ if (lowerStart == upperStart)
+ // Swap with ourself. That was easy!
+ continue;
+
+ long lowerAlbum = songs[lowerStart].albumId;
+ int lowerEnd = lowerStart;
+ while (lowerEnd + 1 < size && songs[lowerEnd + 1].albumId == lowerAlbum)
+ lowerEnd += 1;
+
+ long upperAlbum = songs[upperStart].albumId;
+ int upperEnd = upperStart;
+ while (upperEnd + 1 < size && songs[upperEnd + 1].albumId == upperAlbum)
+ upperEnd += 1;
+
+ int lowerSize = lowerEnd - lowerStart + 1;
+ int upperSize = upperEnd - upperStart + 1;
+
+ if (lowerSize == 1 && upperSize == 1) {
+ // Easy, single element swap
+ Song tempSong = songs[lowerStart];
+ songs[lowerStart] = songs[upperStart];
+ songs[upperStart] = tempSong;
+ } else {
+ // Slow multi-element swap. Copy to a new array in the
+ // swapped order.
+ System.arraycopy(songs, 0, temp, 0, lowerStart); // copy elements before lower
+ System.arraycopy(songs, upperStart, temp, lowerStart, upperSize); // copy upper elements to lower spot
+ System.arraycopy(songs, lowerEnd + 1, temp, lowerStart + upperSize, upperStart - lowerEnd - 1); // copy elements between upper and lower
+ System.arraycopy(songs, lowerStart, temp, lowerStart + upperEnd - lowerEnd, lowerSize); // copy lower elements to upper spot
+ System.arraycopy(songs, upperEnd + 1, temp, upperEnd + 1, size - upperEnd - 1); // copy elements remaining elements after upper
+
+ // New array is finished. Use the old array as temp for the
+ // next iteration.
+ Song[] tempTemp = songs;
+ songs = temp;
+ temp = tempTemp;
+ }
+ }
+
+ list.clear();
+ list.addAll(Arrays.asList(songs));
+ } else {
+ Collections.shuffle(list, random);
+ }
+ }
+
/**
* Determine if any songs are available from the library.
*
diff --git a/src/org/kreed/vanilla/PlaybackActivity.java b/src/org/kreed/vanilla/PlaybackActivity.java
index 9da0e9f8..0ced4a5f 100644
--- a/src/org/kreed/vanilla/PlaybackActivity.java
+++ b/src/org/kreed/vanilla/PlaybackActivity.java
@@ -325,8 +325,22 @@ public class PlaybackActivity extends Activity
public boolean onPrepareOptionsMenu(Menu menu)
{
int state = mState;
- boolean isShuffling = (state & PlaybackService.FLAG_SHUFFLE) != 0;
- menu.findItem(MENU_SHUFFLE).setTitle(isShuffling ? R.string.shuffle_disable : R.string.shuffle_enable);
+
+ int shuffleRes;
+ switch (PlaybackService.shuffleMode(state)) {
+ default:
+ case SongTimeline.SHUFFLE_NONE:
+ shuffleRes = R.string.shuffle_enable;
+ break;
+ case SongTimeline.SHUFFLE_SONGS:
+ shuffleRes = R.string.shuffle_albums;
+ break;
+ case SongTimeline.SHUFFLE_ALBUMS:
+ shuffleRes = R.string.shuffle_disable;
+ break;
+ }
+ menu.findItem(MENU_SHUFFLE).setTitle(shuffleRes);
+
int repeatRes;
if ((state & PlaybackService.FLAG_REPEAT) != 0)
repeatRes = R.string.repeat_current;
@@ -335,9 +349,11 @@ public class PlaybackActivity extends Activity
else
repeatRes = R.string.repeat_enable;
menu.findItem(MENU_REPEAT).setTitle(repeatRes);
+
boolean isRandom = (state & PlaybackService.FLAG_RANDOM) != 0;
// TODO: find icon (dice? arrow pointing in many directions?)
menu.findItem(MENU_RANDOM).setTitle(isRandom ? R.string.random_disable : R.string.random_enable);
+
return true;
}
@@ -346,7 +362,7 @@ public class PlaybackActivity extends Activity
{
switch (item.getItemId()) {
case MENU_SHUFFLE:
- toggleShuffle();
+ cycleShuffle();
return true;
case MENU_REPEAT:
cycleRepeat();
@@ -371,11 +387,23 @@ public class PlaybackActivity extends Activity
/**
* Toggle shuffle mode on/off
*/
- public void toggleShuffle()
+ public void cycleShuffle()
{
- int state = PlaybackService.get(this).toggleShuffle();
- int res = (state & PlaybackService.FLAG_SHUFFLE) == 0 ? R.string.shuffle_disabling : R.string.shuffle_enabling;
- Toast.makeText(this, res, Toast.LENGTH_SHORT).show();
+ int state = PlaybackService.get(this).cycleShuffle();
+ int shuffleRes;
+ switch (PlaybackService.shuffleMode(state)) {
+ default:
+ case SongTimeline.SHUFFLE_NONE:
+ shuffleRes = R.string.shuffle_disabled;
+ break;
+ case SongTimeline.SHUFFLE_SONGS:
+ shuffleRes = R.string.shuffle_songs_enabled;
+ break;
+ case SongTimeline.SHUFFLE_ALBUMS:
+ shuffleRes = R.string.shuffle_albums_enabled;
+ break;
+ }
+ Toast.makeText(this, shuffleRes, Toast.LENGTH_SHORT).show();
setState(state);
}
@@ -441,7 +469,7 @@ public class PlaybackActivity extends Activity
cycleRepeat();
break;
case ACTION_SHUFFLE:
- toggleShuffle();
+ cycleShuffle();
break;
case ACTION_RANDOM:
toggleRandom();
diff --git a/src/org/kreed/vanilla/PlaybackService.java b/src/org/kreed/vanilla/PlaybackService.java
index c6e68f05..12415558 100644
--- a/src/org/kreed/vanilla/PlaybackService.java
+++ b/src/org/kreed/vanilla/PlaybackService.java
@@ -65,7 +65,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
/**
* State file version that indicates data order.
*/
- private static final int STATE_VERSION = 2;
+ private static final int STATE_VERSION = 3;
private static final int NOTIFICATION_ID = 2;
@@ -114,19 +114,14 @@ public final class PlaybackService extends Service implements Handler.Callback,
*/
public static final String ACTION_PREVIOUS_SONG_AUTOPLAY = "org.kreed.vanilla.action.PREVIOUS_SONG_AUTOPLAY";
- /**
- * Set when there is no media available on the device.
- */
- public static final int FLAG_NO_MEDIA = 0x2;
/**
* If set, music will play.
*/
public static final int FLAG_PLAYING = 0x1;
/**
- * If set, songs selected from the library and repeated songs will be in
- * random order.
+ * Set when there is no media available on the device.
*/
- public static final int FLAG_SHUFFLE = 0x4;
+ public static final int FLAG_NO_MEDIA = 0x2;
/**
* If set, will loop back to the beginning of the timeline when its end is
* reached.
@@ -150,6 +145,10 @@ public final class PlaybackService extends Service implements Handler.Callback,
* instead of advancing to the next song.
*/
public static final int FLAG_REPEAT_CURRENT = 0x80;
+ /**
+ * These two bits will be one of SongTimeline.SHUFFLE_*.
+ */
+ public static final int MASK_SHUFFLE = 0x100 | 0x200;
public static final int NEVER = 0;
public static final int WHEN_PLAYING = 1;
@@ -463,8 +462,8 @@ public final class PlaybackService extends Service implements Handler.Callback,
}
}
- if ((toggled & FLAG_SHUFFLE) != 0)
- mTimeline.setShuffle((state & FLAG_SHUFFLE) != 0);
+ if ((toggled & MASK_SHUFFLE) != 0)
+ mTimeline.setShuffleMode(shuffleMode(state));
if ((toggled & (FLAG_REPEAT | FLAG_RANDOM)) != 0) {
int action;
if ((state & FLAG_RANDOM) != 0)
@@ -593,14 +592,15 @@ public final class PlaybackService extends Service implements Handler.Callback,
}
/**
- * Toggle shuffle mode.
+ * Cycle shuffle mode.
*
* @return The new state after this is called.
*/
- public int toggleShuffle()
+ public int cycleShuffle()
{
synchronized (mStateLock) {
- return updateState(mState ^ FLAG_SHUFFLE);
+ int step = (mState & MASK_SHUFFLE) == 0x200 ? 0x200 : 0x100;
+ return updateState(mState + step);
}
}
@@ -1178,8 +1178,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
state |= FLAG_RANDOM;
else if (finishAction == SongTimeline.FINISH_REPEAT)
state |= FLAG_REPEAT;
- if (mTimeline.isShuffling())
- state |= FLAG_SHUFFLE;
+ state |= mTimeline.getShuffleMode() << 8;
state |= savedState & FLAG_REPEAT_CURRENT;
}
@@ -1213,4 +1212,14 @@ public final class PlaybackService extends Service implements Handler.Callback,
Log.w("VanillaMusic", "Failed to save state", e);
}
}
+
+ /**
+ * Returns the shuffle mode for the given state.
+ *
+ * @return The shuffle mode. One of SongTimeline.SHUFFLE_*.
+ */
+ public static int shuffleMode(int state)
+ {
+ return (state & MASK_SHUFFLE) >> 8;
+ }
}
diff --git a/src/org/kreed/vanilla/Song.java b/src/org/kreed/vanilla/Song.java
index d98771f5..f6f5e835 100644
--- a/src/org/kreed/vanilla/Song.java
+++ b/src/org/kreed/vanilla/Song.java
@@ -38,7 +38,7 @@ import java.io.FileNotFoundException;
* Represents a Song backed by the MediaStore. Includes basic metadata and
* utilities to retrieve songs from the MediaStore.
*/
-public class Song {
+public class Song implements Comparable {
/**
* Indicates that this song was randomly selected from all songs.
*/
@@ -61,6 +61,7 @@ public class Song {
MediaStore.Audio.Media.ALBUM_ID,
MediaStore.Audio.Media.ARTIST_ID,
MediaStore.Audio.Media.DURATION,
+ MediaStore.Audio.Media.TRACK,
};
public static final String[] EMPTY_PLAYLIST_PROJECTION = {
@@ -76,6 +77,7 @@ public class Song {
MediaStore.Audio.Playlists.Members.ALBUM_ID,
MediaStore.Audio.Playlists.Members.ARTIST_ID,
MediaStore.Audio.Playlists.Members.DURATION,
+ MediaStore.Audio.Media.TRACK,
};
/**
@@ -123,6 +125,10 @@ public class Song {
* Length of the song in milliseconds.
*/
public long duration;
+ /**
+ * The position of the song in its album.
+ */
+ public int trackNumber;
/**
* Song flags. Currently FLAG_RANDOM or 0.
@@ -171,6 +177,7 @@ public class Song {
albumId = cursor.getLong(5);
artistId = cursor.getLong(6);
duration = cursor.getLong(7);
+ trackNumber = cursor.getInt(8);
}
/**
@@ -245,6 +252,19 @@ public class Song {
@Override
public String toString()
{
- return String.format("%d %s", id, path);
+ return String.format("%d %d", id, albumId);
+ }
+
+ /**
+ * Compares the album ids of the two songs; if equal, compares track order.
+ */
+ @Override
+ public int compareTo(Song other)
+ {
+ if (albumId == other.albumId)
+ return trackNumber - other.trackNumber;
+ if (albumId > other.albumId)
+ return 1;
+ return -1;
}
}
diff --git a/src/org/kreed/vanilla/SongTimeline.java b/src/org/kreed/vanilla/SongTimeline.java
index 0e9384d9..b9161759 100644
--- a/src/org/kreed/vanilla/SongTimeline.java
+++ b/src/org/kreed/vanilla/SongTimeline.java
@@ -81,6 +81,26 @@ public final class SongTimeline {
*/
public static final int MODE_ENQUEUE = 2;
+ /**
+ * Disable shuffle.
+ *
+ * @see SongTimeline#setShuffleMode(int)
+ */
+ public static final int SHUFFLE_NONE = 0;
+ /**
+ * Randomize order of songs.
+ *
+ * @see SongTimeline#setShuffleMode(int)
+ */
+ public static final int SHUFFLE_SONGS = 1;
+ /**
+ * Randomize order of albums, preserving the order of tracks inside the
+ * albums.
+ *
+ * @see SongTimeline#setShuffleMode(int)
+ */
+ public static final int SHUFFLE_ALBUMS = 2;
+
private Context mContext;
/**
* All the songs currently contained in the timeline. Each Song object
@@ -92,10 +112,9 @@ public final class SongTimeline {
*/
private int mCurrentPos;
/**
- * Whether shuffling is enabled. Shuffling will shuffle sets of songs
- * that are added with chooseSongs and shuffle sets of repeated songs.
+ * How to shuffle/whether to shuffle. One of SongTimeline.SHUFFLE_*.
*/
- private boolean mShuffle;
+ private int mShuffleMode;
/**
* What to do when the end of the playlist is reached.
* Must be one of SongTimeline.FINISH_*.
@@ -229,7 +248,7 @@ public final class SongTimeline {
mCurrentPos = Math.min(mSongs == null ? 0 : mSongs.size(), in.readInt());
mFinishAction = in.readInt();
- mShuffle = in.readBoolean();
+ mShuffleMode = in.readInt();
}
}
@@ -261,7 +280,7 @@ public final class SongTimeline {
out.writeInt(mCurrentPos);
out.writeInt(mFinishAction);
- out.writeBoolean(mShuffle);
+ out.writeInt(mShuffleMode);
}
}
@@ -274,11 +293,13 @@ public final class SongTimeline {
}
/**
- * Return whether shuffling is enabled.
+ * Return the current shuffle mode.
+ *
+ * @return The shuffle mode. One of SongTimeline.SHUFFLE_*.
*/
- public boolean isShuffling()
+ public int getShuffleMode()
{
- return mShuffle;
+ return mShuffleMode;
}
/**
@@ -292,25 +313,25 @@ public final class SongTimeline {
}
/**
- * Set whether shuffling is enabled. Will shuffle the current set of songs
- * when enabling shuffling if random mode is not enabled.
+ * Set how to shuffle. Will shuffle the current set of songs when enabling
+ * shuffling if random mode is not enabled.
+ *
+ * @param mode One of SongTimeline.MODE_*
*/
- public void setShuffle(boolean shuffle)
+ public void setShuffleMode(int mode)
{
- if (shuffle == mShuffle)
+ if (mode == mShuffleMode)
return;
synchronized (this) {
saveActiveSongs();
- mShuffle = shuffle;
mShuffledSongs = null;
- if (shuffle && mFinishAction != FINISH_RANDOM) {
+ mShuffleMode = mode;
+ if (mode != SHUFFLE_NONE && mFinishAction != FINISH_RANDOM && mSongs.size() != 0) {
shuffleAll();
ArrayList songs = mShuffledSongs;
mShuffledSongs = null;
- int i = songs.indexOf(mSavedCurrent);
- songs.set(i, songs.get(mCurrentPos));
- songs.set(mCurrentPos, mSavedCurrent);
+ mCurrentPos = songs.indexOf(mSavedCurrent);
mSongs = songs;
}
broadcastChangedSongs();
@@ -343,7 +364,7 @@ public final class SongTimeline {
return mShuffledSongs.get(0);
ArrayList songs = new ArrayList(mSongs);
- Collections.shuffle(songs, MediaUtils.getRandom());
+ MediaUtils.shuffle(songs, mShuffleMode == SHUFFLE_ALBUMS);
mShuffledSongs = songs;
return songs.get(0);
}
@@ -378,7 +399,7 @@ public final class SongTimeline {
if (size == 0)
// empty queue
return null;
- else if (mShuffle)
+ else if (mShuffleMode != SHUFFLE_NONE)
song = shuffleAll();
else
song = timeline.get(0);
@@ -416,7 +437,7 @@ public final class SongTimeline {
int pos = mCurrentPos + delta;
if (mFinishAction != FINISH_RANDOM && pos == mSongs.size()) {
- if (mShuffle && mSongs.size() > 0) {
+ if (mShuffleMode != SHUFFLE_NONE && mSongs.size() > 0) {
if (mShuffledSongs == null)
shuffleAll();
mSongs = mShuffledSongs;
@@ -488,8 +509,8 @@ public final class SongTimeline {
timeline.add(song);
}
- if (mShuffle)
- Collections.shuffle(timeline.subList(start, timeline.size()), MediaUtils.getRandom());
+ if (mShuffleMode != SHUFFLE_NONE)
+ MediaUtils.shuffle(timeline.subList(start, timeline.size()), mShuffleMode == SHUFFLE_ALBUMS);
broadcastChangedSongs();
}