Add album shuffle
This commit is contained in:
parent
f194262c99
commit
370514a316
@ -31,8 +31,10 @@ THE SOFTWARE.
|
||||
<string name="display_mode">Display Mode</string>
|
||||
<string name="shuffle_enable">Enable Shuffle</string>
|
||||
<string name="shuffle_disable">Disable Shuffle</string>
|
||||
<string name="shuffle_enabling">Shuffle enabled</string>
|
||||
<string name="shuffle_disabling">Shuffle disabled</string>
|
||||
<string name="shuffle_albums">Shuffle Albums</string>
|
||||
<string name="shuffle_songs_enabled">Song shuffle enabled</string>
|
||||
<string name="shuffle_albums_enabled">Album shuffle enabled</string>
|
||||
<string name="shuffle_disabled">Shuffle disabled</string>
|
||||
<string name="repeat_enable">Enable Repeat</string>
|
||||
<string name="repeat_current">Repeat Song</string>
|
||||
<string name="repeat_disable">Disable Repeat</string>
|
||||
|
@ -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<Song> 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.
|
||||
*
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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<Song> {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
@ -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<Song> 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<Song> songs = new ArrayList<Song>(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();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user