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(); }