/*
* Copyright (C) 2008 Josh Guilfoyle <jasta@devtcg.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*/
package org.devtcg.five.activity;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import org.devtcg.five.Constants;
import org.devtcg.five.R;
import org.devtcg.five.provider.Five;
import org.devtcg.five.provider.util.AlbumItem;
import org.devtcg.five.provider.util.PlaylistItem;
import org.devtcg.five.service.IPlaylistMoveListener;
import org.devtcg.five.service.IPlaylistService;
import org.devtcg.five.util.PlaylistServiceActivity;
import org.devtcg.five.widget.EfficientCursorAdapter;
import org.devtcg.five.widget.StatefulListView;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.AbstractCursor;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.HeaderViewListAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.SimpleCursorAdapter.ViewBinder;
public class SongList extends PlaylistServiceActivity
implements ViewBinder
{
public static final String TAG = "SongList";
private static final String[] sProjection = {
Five.Music.Songs._ID, Five.Music.Songs.TITLE,
Five.Music.Songs.LENGTH, Five.Music.Songs.TRACK,
Five.Music.Songs.ARTIST_ID
};
/* Compilation albums are handled specially by showing the artist name
* with the song title. This map is designed to efficiently cache
* the result of that extra query. */
private HashMap<Long, String> mVAArtistMap = null;
private SongListExtras mExtras;
private final Handler mHandler = new Handler();
private StatefulListView mList;
private EfficientCursorAdapter mAdapter;
private Cursor mCursor;
private long mSongPlaying = -1;
private static final int MENU_ENQUEUE_LAST = Menu.FIRST;
private static final int MENU_PLAY_NEXT = Menu.FIRST + 1;
private static final int MENU_PLAY_NOW = Menu.FIRST + 2;
private static final int MENU_PAUSE = Menu.FIRST + 3;
private static final int MENU_REPEAT = Menu.FIRST + 4;
private static final int MENU_STOP = Menu.FIRST + 5;
private static final int MENU_PLAY_SHUFFLED = Menu.FIRST + 6;
private static final int MENU_REMOVE = Menu.FIRST + 7;
private static final int MENU_RETURN_LIBRARY = Menu.FIRST + 8;
private static final int MENU_GOTO_PLAYER = Menu.FIRST + 9;
public static Intent makeShowByPlaylistIntent(Context context, PlaylistItem item)
{
Intent chosen = new Intent(Intent.ACTION_VIEW, item.getUri(), context, SongList.class);
chosen.putExtra(Constants.EXTRA_PLAYLIST_ID, item.getId());
chosen.putExtra(Constants.EXTRA_PLAYLIST_NAME, item.getName());
return chosen;
}
public static void showByPlaylist(Context context, PlaylistItem item)
{
context.startActivity(makeShowByPlaylistIntent(context, item));
}
public static void showByArtist(Context context, Uri artistUri, String artistName)
{
Intent chosen = new Intent(Intent.ACTION_VIEW, artistUri, context, SongList.class);
chosen.putExtra(Constants.EXTRA_ARTIST_ID, ContentUris.parseId(artistUri));
chosen.putExtra(Constants.EXTRA_ARTIST_NAME, artistName);
chosen.putExtra(Constants.EXTRA_ALL_ALBUMS, true);
context.startActivity(chosen);
}
public static void showByAlbum(Context context, AlbumItem album)
{
Intent chosen = new Intent(Intent.ACTION_VIEW, album.getUri(), context, SongList.class);
chosen.putExtra(Constants.EXTRA_ARTIST_ID, album.getArtistId());
chosen.putExtra(Constants.EXTRA_ARTIST_NAME, album.getArtist());
chosen.putExtra(Constants.EXTRA_ALBUM_ID, album.getId());
chosen.putExtra(Constants.EXTRA_ALBUM_NAME, album.getName());
chosen.putExtra(Constants.EXTRA_ALBUM_ARTWORK_THUMB, album.getArtworkThumbUri() != null ? album.getArtworkThumbUri().toString() : null);
chosen.putExtra(Constants.EXTRA_ALBUM_ARTWORK_LARGE, album.getArtworkFullUri() != null ? album.getArtworkFullUri().toString() : null);
context.startActivity(chosen);
}
public static void actionOpenPlayQueue(Context context)
{
Intent i = new Intent(context, SongList.class);
i.putExtra(Constants.EXTRA_PLAYQUEUE, true);
context.startActivity(i);
}
private static class SongListExtras
{
public long artistId;
public String artistName;
public long albumId;
public String albumName;
public String albumArt;
public String albumArtBig;
public long playlistId;
public String playlistName;
public boolean allAlbums;
public boolean playQueue;
public SongListExtras(Bundle b)
{
artistId = b.getLong(Constants.EXTRA_ARTIST_ID, -1);
artistName = b.getString(Constants.EXTRA_ARTIST_NAME);
albumId = b.getLong(Constants.EXTRA_ALBUM_ID, -1);
albumName = b.getString(Constants.EXTRA_ALBUM_NAME);
albumArt = b.getString(Constants.EXTRA_ALBUM_ARTWORK_THUMB);
albumArtBig = b.getString(Constants.EXTRA_ALBUM_ARTWORK_LARGE);
if (b.containsKey(Constants.EXTRA_PLAYLIST_ID) == true)
{
playlistId = b.getLong(Constants.EXTRA_PLAYLIST_ID);
playlistName = b.getString(Constants.EXTRA_PLAYLIST_NAME);
}
else
{
playlistId = -1;
playlistName = null;
}
allAlbums = b.getBoolean(Constants.EXTRA_ALL_ALBUMS);
playQueue = b.getBoolean(Constants.EXTRA_PLAYQUEUE);
}
public boolean showAlbumCover()
{
if (allAlbums == true || playlistId >= 0 || playQueue == true)
return false;
return true;
}
public boolean showTrackNumbers()
{
return showAlbumCover();
}
public boolean hasMultipleArtists()
{
if (playlistId >= 0 || playQueue == true)
return true;
return artistName.equals("Various Artists");
}
}
@Override
public void onCreate(Bundle icicle)
{
super.onCreate(icicle);
mExtras = new SongListExtras(getIntent().getExtras());
if (mExtras.showAlbumCover() == true)
requestWindowFeature(Window.FEATURE_NO_TITLE);
else
{
if (mExtras.allAlbums == true)
setTitle(mExtras.artistName);
else if (mExtras.playlistId >= 0)
setTitle(mExtras.playlistName);
else if (mExtras.playQueue == true)
setTitle("Now Playing");
else
{
Log.w(TAG, "Uncertain invocation, aborting...");
finish();
}
}
}
@Override
protected void onInitUI()
{
setContentView(R.layout.song_list);
Intent i = getIntent();
if (mExtras.playQueue == true)
{
mCursor = new PlayQueueCursor(mService, sProjection);
startManagingCursor(mCursor);
}
else
{
Uri songsUri = i.getData().buildUpon()
.appendPath("songs").build();
mCursor = managedQuery(songsUri, sProjection, null, null, null);
}
if (mExtras.hasMultipleArtists() == true)
mVAArtistMap = new HashMap<Long, String>(mCursor.getCount());
mList = (StatefulListView)findViewById(android.R.id.list);
if (mExtras.showAlbumCover() == true)
{
AlbumInfoView infoView = new AlbumInfoView(this);
infoView.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.FILL_PARENT, AbsListView.LayoutParams.WRAP_CONTENT));
infoView.bindToData(mExtras);
mList.addHeaderView(infoView, null, false);
}
mAdapter = new EfficientCursorAdapter(this,
R.layout.song_list_item, mCursor,
new String[] { "title", "artist_id", "_id" },
new int[] { R.id.song_name, R.id.artist_name, R.id.song_playing_icon });
mAdapter.setViewBinder(this);
mList.setOnItemClickListener(mOnItemClickListener);
registerForContextMenu(mList);
mList.setAdapter(mAdapter);
if (mExtras.playQueue == true)
{
try {
mList.setSelection(mService.getPosition());
} catch (RemoteException e) {}
}
}
public String getArtistName(long artistId)
{
Uri artistUri = Five.Music.Artists.CONTENT_URI.buildUpon()
.appendEncodedPath(String.valueOf(artistId)).build();
Cursor c = getContentResolver().query(artistUri,
new String[] { Five.Music.Artists.NAME }, null, null, null);
try {
if (c.moveToFirst() == false)
return null;
return c.getString(0);
} finally {
c.close();
}
}
public boolean setViewValue(View v, Cursor c, int col)
{
if (sProjection[col] == Five.Music.Songs._ID)
{
if (mSongPlaying >= 0 && c.getLong(col) == mSongPlaying)
v.setVisibility(View.VISIBLE);
else
v.setVisibility(View.INVISIBLE);
}
else if (sProjection[col] == Five.Music.Songs.ARTIST_ID)
{
if (mVAArtistMap != null)
{
long artistId = c.getLong(col);
String artist = mVAArtistMap.get(artistId);
if (artist == null)
{
artist = getArtistName(artistId);
if (artist == null)
throw new IllegalStateException("What the fuck?");
mVAArtistMap.put(artistId, artist);
}
((TextView)v).setText(artist);
v.setVisibility(View.VISIBLE);
}
}
else
{
return false;
}
return true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
super.onCreateOptionsMenu(menu);
menu.add(0, MENU_RETURN_LIBRARY, 0, R.string.return_library)
.setIcon(R.drawable.ic_menu_music_library);
menu.add(0, MENU_GOTO_PLAYER, 0, R.string.goto_player)
.setIcon(R.drawable.ic_menu_playback);
menu.add(0, MENU_PLAY_SHUFFLED, 0, R.string.shuffle_all)
.setIcon(R.drawable.ic_menu_shuffle);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu)
{
menu.findItem(MENU_GOTO_PLAYER).setVisible(mSongPlaying >= 0);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
try {
switch (item.getItemId())
{
case MENU_RETURN_LIBRARY:
Main.show(this);
return true;
case MENU_GOTO_PLAYER:
Player.show(this);
return true;
case MENU_PLAY_SHUFFLED:
playSongsShuffled();
return true;
}
} catch (RemoteException e) {}
return false;
}
@Override
protected void onAttached()
{
super.onAttached();
try {
mService.registerOnMoveListener(mPlaylistMoveListener);
long songId;
if (mService.isPlaying() == false)
songId = -1;
else
{
int pos = mService.getPosition();
songId = mService.getSongAt(pos);
}
setPlaying(songId);
} catch (RemoteException e) {}
}
@Override
protected void onDetached()
{
super.onDetached();
try {
mService.unregisterOnMoveListener(mPlaylistMoveListener);
} catch (RemoteException e) {}
}
private static class AlbumInfoView extends LinearLayout
{
public AlbumInfoView(Context ctx)
{
super(ctx);
LayoutInflater.from(ctx)
.inflate(R.layout.album_header, this);
setPadding(0, 0, 0, 0);
}
public void bindToData(String artist, String album, String artwork)
{
((TextView)findViewById(R.id.album_name)).setText(album);
ImageView artworkView = (ImageView)findViewById(R.id.album_cover);
if (artwork != null)
artworkView.setImageURI(Uri.parse(artwork));
else
artworkView.setImageResource(R.drawable.lastfm_cover_small);
}
public void bindToData(SongListExtras e)
{
bindToData(e.artistName, e.albumName, e.albumArt);
}
public void bindToData(Cursor c)
{
bindToData(c.getString(c.getColumnIndexOrThrow(Five.Music.Artists.NAME)),
c.getString(c.getColumnIndexOrThrow(Five.Music.Albums.FULL_NAME)),
c.getString(c.getColumnIndexOrThrow(Five.Music.Albums.ARTWORK)));
}
}
private void markRowPlaying(long songId, boolean playing)
{
View row = mList.getChildFromId(songId);
if (row == null)
return;
int vis = playing ? View.VISIBLE : View.INVISIBLE;
row.findViewById(R.id.song_playing_icon).setVisibility(vis);
}
private void setPlaying(long songId)
{
if (mSongPlaying >= 0)
markRowPlaying(mSongPlaying, false);
if (songId >= 0)
markRowPlaying(songId, true);
mSongPlaying = songId;
}
private IPlaylistMoveListener.Stub mPlaylistMoveListener = new IPlaylistMoveListener.Stub()
{
public void onAdvance() throws RemoteException
{
}
public void onJump(int pos) throws RemoteException
{
final long songId = mService.getSongAt(pos);
mHandler.post(new Runnable() {
public void run() {
setPlaying(songId);
}
});
}
public void onPause() throws RemoteException
{
}
public void onPlay() throws RemoteException
{
int pos = mService.getPosition();
final long songId = mService.getSongAt(pos);
mHandler.post(new Runnable() {
public void run() {
setPlaying(songId);
}
});
}
public void onSeek(long pos) throws RemoteException
{
}
public void onStop() throws RemoteException
{
mHandler.post(new Runnable() {
public void run() {
setPlaying(-1);
}
});
}
public void onUnpause() throws RemoteException
{
}
};
@Override
public boolean onContextItemSelected(MenuItem item)
{
AdapterContextMenuInfo info = (AdapterContextMenuInfo)item.getMenuInfo();
long songId = info.id;
int pos = info.position;
try
{
switch (item.getItemId())
{
case MENU_STOP:
mService.stop();
return true;
case MENU_PAUSE:
if (mService.isPaused() == false)
mService.pause();
else
mService.unpause();
return true;
case MENU_REPEAT:
Toast.makeText(this, "Not supported",
Toast.LENGTH_SHORT).show();
return true;
case MENU_PLAY_NEXT:
insertOneSongNext(pos, songId);
return true;
case MENU_PLAY_NOW:
playOneSong(pos, songId);
return true;
case MENU_ENQUEUE_LAST:
appendOneSong(pos, songId);
return true;
}
}
catch (RemoteException e) {}
return false;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenu.ContextMenuInfo menuInfo)
{
AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
if (info.id == -1)
return;
Cursor c = (Cursor)mAdapter.getItem(info.position -
mList.getHeaderViewsCount());
String trackName =
c.getString(c.getColumnIndex(Five.Music.Songs.TITLE));
menu.setHeaderTitle(trackName);
if (mSongPlaying == info.id)
{
menu.add(0, MENU_STOP, Menu.NONE, "Stop");
try {
if (mService.isPaused() == false)
menu.add(0, MENU_PAUSE, Menu.NONE, "Pause");
else
menu.add(0, MENU_PAUSE, Menu.NONE, "Unpause");
} catch (RemoteException e) {}
menu.add(0, MENU_REPEAT, Menu.NONE, "Repeat once");
}
else
{
menu.add(0, MENU_PLAY_NEXT, Menu.NONE, "Play next");
menu.add(0, MENU_PLAY_NOW, Menu.NONE, "Play now");
if (mExtras.playQueue == false)
menu.add(0, MENU_ENQUEUE_LAST, Menu.NONE, "Enqueue");
}
// if (mExtras.playQueue == true)
// menu.add(0, MENU_REMOVE, Menu.NONE, "Remove from playlist");
};
/* Gets an Intent to move to the player screen with plenty of optimization
* hints. */
private Intent getHintedIntent(int pos, long id)
{
Intent i = new Intent(SongList.this, Player.class);
if (mExtras.showAlbumCover() == false)
{
Log.w(TAG, "TODO: We don't pre-hint without the album queried out.");
return i;
}
if (pos < 0 || id < 0)
{
Log.w(TAG, "TODO: Hinting isn't covered in all cases where it could be.");
return i;
}
Cursor c = (Cursor)mAdapter.getItem(pos);
if (mVAArtistMap != null)
{
long artistId =
c.getLong(c.getColumnIndexOrThrow(Five.Music.Songs.ARTIST_ID));
i.putExtra(Constants.EXTRA_ARTIST_ID, artistId);
i.putExtra(Constants.EXTRA_ARTIST_NAME, mVAArtistMap.get(artistId));
}
else
{
i.putExtra(Constants.EXTRA_ARTIST_ID, mExtras.artistId);
i.putExtra(Constants.EXTRA_ARTIST_NAME, mExtras.artistName);
}
i.putExtra(Constants.EXTRA_ALBUM_ID, mExtras.albumId);
i.putExtra(Constants.EXTRA_ALBUM_NAME, mExtras.albumName);
i.putExtra(Constants.EXTRA_ALBUM_ARTWORK_LARGE, mExtras.albumArtBig);
i.putExtra(Constants.EXTRA_SONG_ID, id);
i.putExtra(Constants.EXTRA_SONG_TITLE,
c.getString(c.getColumnIndexOrThrow(Five.Music.Songs.TITLE)));
i.putExtra(Constants.EXTRA_SONG_LENGTH,
c.getLong(c.getColumnIndexOrThrow(Five.Music.Songs.LENGTH)));
i.putExtra(Constants.EXTRA_PLAYLIST_POSITION, pos);
i.putExtra(Constants.EXTRA_PLAYLIST_LENGTH, mAdapter.getCount());
return i;
}
private void playerStart(int pos, long id)
throws RemoteException
{
mService.unregisterOnMoveListener(mPlaylistMoveListener);
startActivity(getHintedIntent(pos, id));
}
private void playSongsShuffled()
throws RemoteException
{
playerStart(-1, -1);
mService.clear();
Random r = new Random();
int n = mAdapter.getCount();
long[] playlist = new long[n];
for (int i = 0; i < n; i++)
playlist[i] = mAdapter.getItemId(i);
while (n > 0)
{
int k = r.nextInt(n);
n--;
mService.append(playlist[k]);
playlist[k] = playlist[n];
}
}
private void playSongsStartingAt(int pos, long id)
throws RemoteException
{
playerStart(pos, id);
if (mExtras.playQueue == true)
{
mService.jump(pos);
return;
}
mService.clear();
mService.append(id);
for (int i = pos - 1; i >= 0; i--)
mService.prepend(mAdapter.getItemId(i));
int n = mAdapter.getCount();
for (int i = pos + 1; i < n; i++)
mService.append(mAdapter.getItemId(i));
}
private void playExistingSongAt(int pos, long id, int currPos)
throws RemoteException
{
if (mSongPlaying != id || mExtras.playQueue == true)
playSongsStartingAt(pos, id);
else
{
playerStart(pos, id);
/* Delete the current playlist (except the playing song). */
int currN = mService.getPlaylistLength();
while (currN-- > 0)
{
if (currN == currPos)
continue;
mService.remove(currN);
}
/* Then build the playlist up again around selected song. */
for (int i = pos - 1; i >= 0; i--)
mService.prepend(mAdapter.getItemId(i));
int newN = mAdapter.getCount();
for (int i = pos + 1; i < newN; i++)
mService.append(mAdapter.getItemId(i));
}
}
private void insertOneSongNext(int pos, long id)
throws RemoteException
{
mService.insertNext(id);
}
private void appendOneSong(int pos, long id)
throws RemoteException
{
mService.append(id);
}
private void playOneSong(int pos, long id)
throws RemoteException
{
playerStart(pos, id);
if (mExtras.playQueue == true)
mService.jump(pos);
else
{
mService.clear();
mService.append(id);
}
}
private OnItemClickListener mOnItemClickListener = new OnItemClickListener()
{
public void onItemClick(AdapterView<?> av, View v, int pos, long id)
{
/* Generalized logic to ignore header clicks... */
Object ad = av.getAdapter();
if (ad instanceof HeaderViewListAdapter)
{
HeaderViewListAdapter adapter = (HeaderViewListAdapter)ad;
int clickpos = pos;
pos -= adapter.getHeadersCount();
if (pos < 0)
{
/* We only set an item for the "Shuffle" header... */
if (adapter.getItem(clickpos) != null)
{
try {
playSongsShuffled();
} catch (RemoteException e) {}
}
return;
}
}
Log.i(TAG, "Clicked pos=" + pos);
try
{
int found;
if ((found = mService.getPositionOf(id)) >= 0)
playExistingSongAt(pos, id, found);
else
playSongsStartingAt(pos, id);
}
catch (RemoteException e) {}
}
};
private class PlayQueueCursor extends AbstractCursor
{
/* The database cursor that represents this selection of songs, in
* _id order. */
protected Cursor mWrapped;
protected String[] mFields;
/* The wrapped cursor is not ordered the same as our play queue,
* so we must maintain an index. This maps queue position to
* cursor position. */
protected int[] mPositions;
protected int mCount;
public PlayQueueCursor(IPlaylistService service, String[] fields)
{
super();
mFields = fields;
mService = service;
init();
}
private void init()
{
List queue;
try {
queue = mService.getPlaylist();
} catch (RemoteException e) {
return;
}
if (queue.isEmpty() == true)
return;
StringBuilder where = new StringBuilder();
where.append(Five.Music.Songs._ID + " IN (");
for (int n = queue.size() - 1; n >= 0; n--)
{
where.append(queue.get(n));
if (n > 0)
where.append(',');
}
where.append(')');
/* We can't get a cursor in the order that we want so
* we'll just order by _ID and do binary searches.
* XXX: It would be more efficient to perform these searches
* directly on the cursor in onMove rather than precomputing
* the indices here. */
mWrapped = SongList.this.getContentResolver()
.query(Five.Music.Songs.CONTENT_URI, mFields,
where.toString(), null, Five.Music.Songs._ID);
mCount = mWrapped.getCount();
mPositions = new int[mCount];
/* Make one pass to build a searchable array by _ID. */
long[] songIdx = new long[mCount];
for (int i = 0; i < mCount; i++)
{
mWrapped.moveToNext();
/* XXX: We assume column 0 is the _ID column. */
songIdx[i] = mWrapped.getLong(0);
}
/* ...then build our queue to cursor position mapping. */
for (int i = 0; i < mCount; i++)
{
mPositions[i] =
Arrays.binarySearch(songIdx, (Long)queue.get(i));
}
}
@Override
public boolean onMove(int oldPosition, int newPosition)
{
if (oldPosition == newPosition)
return true;
if (mWrapped == null)
return false;
mWrapped.moveToPosition(mPositions[newPosition]);
return true;
}
@Override
public void deactivate()
{
if (mWrapped != null)
{
mWrapped.deactivate();
mWrapped = null;
mPositions = null;
}
}
@Override
public boolean requery()
{
close();
init();
return true;
}
@Override
public void close()
{
if (mWrapped != null)
{
mWrapped.close();
mWrapped = null;
mPositions = null;
}
}
@Override
public String[] getColumnNames()
{
return mFields;
}
@Override
public int getCount()
{
return mCount;
}
@Override
public double getDouble(int column)
{
return mWrapped.getDouble(column);
}
@Override
public float getFloat(int column)
{
return mWrapped.getFloat(column);
}
@Override
public int getInt(int column)
{
return mWrapped.getInt(column);
}
@Override
public long getLong(int column)
{
return mWrapped.getLong(column);
}
@Override
public short getShort(int column)
{
return mWrapped.getShort(column);
}
@Override
public String getString(int column)
{
return mWrapped.getString(column);
}
@Override
public boolean isNull(int column)
{
return mWrapped.isNull(column);
}
}
}