Add swipe actions to enqueue songs
This commit is contained in:
parent
027489b4a1
commit
8172618a09
@ -50,6 +50,9 @@ THE SOFTWARE.
|
|||||||
<item>Previous Song</item>
|
<item>Previous Song</item>
|
||||||
<item>Toggle Repeat</item>
|
<item>Toggle Repeat</item>
|
||||||
<item>Toggle Shuffle</item>
|
<item>Toggle Shuffle</item>
|
||||||
|
<item>Enqueue Current Album</item>
|
||||||
|
<item>Enqueue Current Artist</item>
|
||||||
|
<item>Enqueue Current Genre</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="entry_values">
|
<string-array name="entry_values">
|
||||||
<!-- Note: even if this was an integer-array, these would be converted
|
<!-- Note: even if this was an integer-array, these would be converted
|
||||||
@ -62,5 +65,8 @@ THE SOFTWARE.
|
|||||||
<item>4</item>
|
<item>4</item>
|
||||||
<item>5</item>
|
<item>5</item>
|
||||||
<item>6</item>
|
<item>6</item>
|
||||||
|
<item>7</item>
|
||||||
|
<item>8</item>
|
||||||
|
<item>9</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -38,6 +38,10 @@ THE SOFTWARE.
|
|||||||
<string name="repeat_enabling">Repeat enabled. The current song and any songs you have added after it will be repeated.</string>
|
<string name="repeat_enabling">Repeat enabled. The current song and any songs you have added after it will be repeated.</string>
|
||||||
<string name="repeat_disabling">Repeat disabled</string>
|
<string name="repeat_disabling">Repeat disabled</string>
|
||||||
<string name="song_load_failed">Failed to load song %s. It may be corrupt or missing.</string>
|
<string name="song_load_failed">Failed to load song %s. It may be corrupt or missing.</string>
|
||||||
|
<plurals name="enqueued_count">
|
||||||
|
<item quantity="one">1 song enqueued.</item>
|
||||||
|
<item quantity="other">%d songs enqueued.</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
<!-- Widgets -->
|
<!-- Widgets -->
|
||||||
<string name="starting">Starting up...</string>
|
<string name="starting">Starting up...</string>
|
||||||
|
@ -59,30 +59,39 @@ public class MediaUtils {
|
|||||||
* @param type One of the TYPE_* constants, excluding playlists.
|
* @param type One of the TYPE_* constants, excluding playlists.
|
||||||
* @param id The MediaStore id of the artist or album.
|
* @param id The MediaStore id of the artist or album.
|
||||||
* @param projection The columns to query.
|
* @param projection The columns to query.
|
||||||
|
* @param select An extra selection to pass to the query, or null.
|
||||||
*/
|
*/
|
||||||
private static Cursor getMediaCursor(int type, long id, String[] projection)
|
private static Cursor getMediaCursor(int type, long id, String[] projection, String select)
|
||||||
{
|
{
|
||||||
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
|
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
|
||||||
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||||
String selection;
|
StringBuilder selection = new StringBuilder();
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TYPE_SONG:
|
case TYPE_SONG:
|
||||||
selection = MediaStore.Audio.Media._ID;
|
selection.append(MediaStore.Audio.Media._ID);
|
||||||
break;
|
break;
|
||||||
case TYPE_ARTIST:
|
case TYPE_ARTIST:
|
||||||
selection = MediaStore.Audio.Media.ARTIST_ID;
|
selection.append(MediaStore.Audio.Media.ARTIST_ID);
|
||||||
break;
|
break;
|
||||||
case TYPE_ALBUM:
|
case TYPE_ALBUM:
|
||||||
selection = MediaStore.Audio.Media.ALBUM_ID;
|
selection.append(MediaStore.Audio.Media.ALBUM_ID);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Invalid type specified: " + type);
|
throw new IllegalArgumentException("Invalid type specified: " + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
selection += "=" + id + " AND " + MediaStore.Audio.Media.IS_MUSIC + "!=0";
|
selection.append('=');
|
||||||
|
selection.append(id);
|
||||||
|
selection.append(" AND is_music!=0");
|
||||||
|
|
||||||
|
if (select != null) {
|
||||||
|
selection.append(" AND ");
|
||||||
|
selection.append(select);
|
||||||
|
}
|
||||||
|
|
||||||
String sort = MediaStore.Audio.Media.ARTIST_KEY + ',' + MediaStore.Audio.Media.ALBUM_KEY + ',' + MediaStore.Audio.Media.TRACK;
|
String sort = MediaStore.Audio.Media.ARTIST_KEY + ',' + MediaStore.Audio.Media.ALBUM_KEY + ',' + MediaStore.Audio.Media.TRACK;
|
||||||
return resolver.query(media, projection, selection, null, sort);
|
return resolver.query(media, projection, selection.toString(), null, sort);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -124,24 +133,28 @@ public class MediaUtils {
|
|||||||
* constants.
|
* constants.
|
||||||
* @param id The id of the element in the MediaStore content provider for
|
* @param id The id of the element in the MediaStore content provider for
|
||||||
* the given type.
|
* the given type.
|
||||||
|
* @param selection An extra selection to be passed to the query. May be
|
||||||
|
* null. Must not be used with type == TYPE_SONG or type == TYPE_PLAYLIST
|
||||||
*/
|
*/
|
||||||
public static long[] getAllSongIdsWith(int type, long id)
|
public static long[] getAllSongIdsWith(int type, long id, String selection)
|
||||||
{
|
{
|
||||||
Cursor cursor;
|
Cursor cursor;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TYPE_SONG:
|
case TYPE_SONG:
|
||||||
|
assert(selection == null);
|
||||||
return new long[] { id };
|
return new long[] { id };
|
||||||
case TYPE_ARTIST:
|
case TYPE_ARTIST:
|
||||||
case TYPE_ALBUM:
|
case TYPE_ALBUM:
|
||||||
cursor = getMediaCursor(type, id, new String[] { MediaStore.Audio.Media._ID });
|
cursor = getMediaCursor(type, id, new String[] { MediaStore.Audio.Media._ID }, selection);
|
||||||
break;
|
break;
|
||||||
case TYPE_PLAYLIST:
|
case TYPE_PLAYLIST:
|
||||||
|
assert(selection == null);
|
||||||
cursor = getPlaylistCursor(id, new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID });
|
cursor = getPlaylistCursor(id, new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID });
|
||||||
break;
|
break;
|
||||||
case TYPE_GENRE:
|
case TYPE_GENRE:
|
||||||
// NOTE: AUDIO_ID does not seem to work here, strangely.
|
// NOTE: AUDIO_ID does not seem to work here, strangely.
|
||||||
cursor = queryGenre(id, new String[] { MediaStore.Audio.Genres.Members._ID }, null, null);
|
cursor = queryGenre(id, new String[] { MediaStore.Audio.Genres.Members._ID }, selection, null);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Specified type not valid: " + type);
|
throw new IllegalArgumentException("Specified type not valid: " + type);
|
||||||
@ -178,7 +191,7 @@ public class MediaUtils {
|
|||||||
|
|
||||||
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
|
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
|
||||||
String[] projection = new String [] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA };
|
String[] projection = new String [] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA };
|
||||||
Cursor cursor = getMediaCursor(type, id, projection);
|
Cursor cursor = getMediaCursor(type, id, projection, null);
|
||||||
|
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
PlaybackService service = ContextApplication.hasService() ? ContextApplication.getService() : null;
|
PlaybackService service = ContextApplication.hasService() ? ContextApplication.getService() : null;
|
||||||
@ -200,6 +213,42 @@ public class MediaUtils {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query the MediaStore to determine the id of the genre the song belongs
|
||||||
|
* to.
|
||||||
|
*/
|
||||||
|
public static long queryGenreForSong(long id)
|
||||||
|
{
|
||||||
|
// This is terribly inefficient, but it seems to be the only way to do
|
||||||
|
// this. Honeycomb introduced an API to query the genre of the song.
|
||||||
|
// We should look into it when ICS is released.
|
||||||
|
|
||||||
|
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
|
||||||
|
|
||||||
|
// query ids of all the genres
|
||||||
|
Uri uri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI;
|
||||||
|
String[] projection = { "_id" };
|
||||||
|
Cursor cursor = resolver.query(uri, projection, null, null, null);
|
||||||
|
|
||||||
|
if (cursor != null) {
|
||||||
|
String selection = "_id=" + id;
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
// check if the given song belongs to this genre
|
||||||
|
long genreId = cursor.getLong(0);
|
||||||
|
Uri genreUri = MediaStore.Audio.Genres.Members.getContentUri("external", genreId);
|
||||||
|
Cursor c = resolver.query(genreUri, projection, selection, null, null);
|
||||||
|
if (c != null) {
|
||||||
|
if (c.getCount() == 1)
|
||||||
|
return genreId;
|
||||||
|
c.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle an array using Fisher-Yates algorithm.
|
* Shuffle an array using Fisher-Yates algorithm.
|
||||||
*
|
*
|
||||||
|
@ -36,6 +36,7 @@ import android.view.KeyEvent;
|
|||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
public class PlaybackActivity extends Activity implements Handler.Callback, View.OnClickListener, CoverView.Callback {
|
public class PlaybackActivity extends Activity implements Handler.Callback, View.OnClickListener, CoverView.Callback {
|
||||||
public static final int ACTION_NOTHING = 0;
|
public static final int ACTION_NOTHING = 0;
|
||||||
@ -45,6 +46,9 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View
|
|||||||
public static final int ACTION_PREVIOUS_SONG = 4;
|
public static final int ACTION_PREVIOUS_SONG = 4;
|
||||||
public static final int ACTION_REPEAT = 5;
|
public static final int ACTION_REPEAT = 5;
|
||||||
public static final int ACTION_SHUFFLE = 6;
|
public static final int ACTION_SHUFFLE = 6;
|
||||||
|
public static final int ACTION_ENQUEUE_ALBUM = 7;
|
||||||
|
public static final int ACTION_ENQUEUE_ARTIST = 8;
|
||||||
|
public static final int ACTION_ENQUEUE_GENRE = 9;
|
||||||
|
|
||||||
public static int mUpAction;
|
public static int mUpAction;
|
||||||
public static int mDownAction;
|
public static int mDownAction;
|
||||||
@ -359,6 +363,13 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View
|
|||||||
startActivity(new Intent(this, SongSelector.class));
|
startActivity(new Intent(this, SongSelector.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void enqueue(int type)
|
||||||
|
{
|
||||||
|
int count = ContextApplication.getService().enqueueFromCurrent(type);
|
||||||
|
String text = getResources().getQuantityString(R.plurals.enqueued_count, count, count);
|
||||||
|
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
|
||||||
public void performAction(int action)
|
public void performAction(int action)
|
||||||
{
|
{
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@ -382,6 +393,15 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View
|
|||||||
case ACTION_SHUFFLE:
|
case ACTION_SHUFFLE:
|
||||||
toggleShuffle();
|
toggleShuffle();
|
||||||
break;
|
break;
|
||||||
|
case ACTION_ENQUEUE_ALBUM:
|
||||||
|
enqueue(MediaUtils.TYPE_ALBUM);
|
||||||
|
break;
|
||||||
|
case ACTION_ENQUEUE_ARTIST:
|
||||||
|
enqueue(MediaUtils.TYPE_ARTIST);
|
||||||
|
break;
|
||||||
|
case ACTION_ENQUEUE_GENRE:
|
||||||
|
enqueue(MediaUtils.TYPE_GENRE);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Invalid action: " + action);
|
throw new IllegalArgumentException("Invalid action: " + action);
|
||||||
}
|
}
|
||||||
|
@ -981,7 +981,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
|
|||||||
*/
|
*/
|
||||||
public Song playSongs(int type, long id)
|
public Song playSongs(int type, long id)
|
||||||
{
|
{
|
||||||
mTimeline.chooseSongs(false, type, id);
|
mTimeline.chooseSongs(false, type, id, null);
|
||||||
return setCurrentSong(+1);
|
return setCurrentSong(+1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -999,11 +999,48 @@ public final class PlaybackService extends Service implements Handler.Callback,
|
|||||||
*/
|
*/
|
||||||
public void enqueueSongs(int type, long id)
|
public void enqueueSongs(int type, long id)
|
||||||
{
|
{
|
||||||
mTimeline.chooseSongs(true, type, id);
|
mTimeline.chooseSongs(true, type, id, null);
|
||||||
mHandler.removeMessages(SAVE_STATE);
|
mHandler.removeMessages(SAVE_STATE);
|
||||||
mHandler.sendEmptyMessageDelayed(SAVE_STATE, 5000);
|
mHandler.sendEmptyMessageDelayed(SAVE_STATE, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueues all the songs with the same album/artist/genre as the current
|
||||||
|
* song.
|
||||||
|
*
|
||||||
|
* This will clear the queue and place the first song from the group after
|
||||||
|
* the playing song.
|
||||||
|
*
|
||||||
|
* @param type The media type, one of MediaUtils.TYPE_ALBUM, TYPE_ARTIST,
|
||||||
|
* or TYPE_GENRE
|
||||||
|
* @return The number of songs that were enqueued.
|
||||||
|
*/
|
||||||
|
public int enqueueFromCurrent(int type)
|
||||||
|
{
|
||||||
|
Song current = getSong(0);
|
||||||
|
if (current == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
long id = -1;
|
||||||
|
switch (type) {
|
||||||
|
case MediaUtils.TYPE_ARTIST:
|
||||||
|
id = current.artistId;
|
||||||
|
break;
|
||||||
|
case MediaUtils.TYPE_ALBUM:
|
||||||
|
id = current.albumId;
|
||||||
|
break;
|
||||||
|
case MediaUtils.TYPE_GENRE:
|
||||||
|
id = MediaUtils.queryGenreForSong(current.id);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported media type: " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
String selection = "_id!=" + current.id;
|
||||||
|
int count = mTimeline.chooseSongs(false, type, id, selection);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the position at which songs are enqueued. That is, the next song
|
* Reset the position at which songs are enqueued. That is, the next song
|
||||||
* enqueued will be placed directly after the playing song.
|
* enqueued will be placed directly after the playing song.
|
||||||
|
@ -86,6 +86,7 @@ public class Song implements Parcelable {
|
|||||||
MediaStore.Audio.Media.ALBUM,
|
MediaStore.Audio.Media.ALBUM,
|
||||||
MediaStore.Audio.Media.ARTIST,
|
MediaStore.Audio.Media.ARTIST,
|
||||||
MediaStore.Audio.Media.ALBUM_ID,
|
MediaStore.Audio.Media.ALBUM_ID,
|
||||||
|
MediaStore.Audio.Media.ARTIST_ID,
|
||||||
MediaStore.Audio.Media.DURATION
|
MediaStore.Audio.Media.DURATION
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -97,6 +98,10 @@ public class Song implements Parcelable {
|
|||||||
* Id of this song's album in the MediaStore
|
* Id of this song's album in the MediaStore
|
||||||
*/
|
*/
|
||||||
public long albumId;
|
public long albumId;
|
||||||
|
/**
|
||||||
|
* Id of this song's artist in the MediaStore
|
||||||
|
*/
|
||||||
|
public long artistId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to the data for this song
|
* Path to the data for this song
|
||||||
@ -293,7 +298,8 @@ public class Song implements Parcelable {
|
|||||||
album = cursor.getString(3);
|
album = cursor.getString(3);
|
||||||
artist = cursor.getString(4);
|
artist = cursor.getString(4);
|
||||||
albumId = cursor.getLong(5);
|
albumId = cursor.getLong(5);
|
||||||
duration = cursor.getLong(6);
|
artistId = cursor.getLong(6);
|
||||||
|
duration = cursor.getLong(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -466,7 +466,7 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
|
|||||||
*/
|
*/
|
||||||
private void addToPlaylist(long playlistId, int type, long mediaId, CharSequence title)
|
private void addToPlaylist(long playlistId, int type, long mediaId, CharSequence title)
|
||||||
{
|
{
|
||||||
long[] ids = MediaUtils.getAllSongIdsWith(type, mediaId);
|
long[] ids = MediaUtils.getAllSongIdsWith(type, mediaId, null);
|
||||||
Playlist.addToPlaylist(playlistId, ids);
|
Playlist.addToPlaylist(playlistId, ids);
|
||||||
|
|
||||||
String message = getResources().getQuantityString(R.plurals.added_to_playlist, ids.length, ids.length, title);
|
String message = getResources().getQuantityString(R.plurals.added_to_playlist, ids.length, ids.length, title);
|
||||||
|
@ -367,12 +367,15 @@ public final class SongTimeline {
|
|||||||
* MediaUtils.FIELD_* constants.
|
* MediaUtils.FIELD_* constants.
|
||||||
* @param id The id of the element in the MediaStore content provider for
|
* @param id The id of the element in the MediaStore content provider for
|
||||||
* the given type.
|
* the given type.
|
||||||
|
* @param selection An extra selection to be passed to the query. May be
|
||||||
|
* null. Must not be used with type == TYPE_SONG or type == TYPE_PLAYLIST
|
||||||
|
* @return The number of songs that were enqueued.
|
||||||
*/
|
*/
|
||||||
public void chooseSongs(boolean enqueue, int type, long id)
|
public int chooseSongs(boolean enqueue, int type, long id, String selection)
|
||||||
{
|
{
|
||||||
long[] songs = MediaUtils.getAllSongIdsWith(type, id);
|
long[] songs = MediaUtils.getAllSongIdsWith(type, id, selection);
|
||||||
if (songs == null || songs.length == 0)
|
if (songs == null || songs.length == 0)
|
||||||
return;
|
return 0;
|
||||||
|
|
||||||
if (mShuffle)
|
if (mShuffle)
|
||||||
MediaUtils.shuffle(songs);
|
MediaUtils.shuffle(songs);
|
||||||
@ -404,6 +407,8 @@ public final class SongTimeline {
|
|||||||
Song newSong = getSong(+1);
|
Song newSong = getSong(+1);
|
||||||
if (newSong != oldSong && mCallback != null)
|
if (newSong != oldSong && mCallback != null)
|
||||||
mCallback.songReplaced(+1, newSong);
|
mCallback.songReplaced(+1, newSong);
|
||||||
|
|
||||||
|
return songs.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user