/* * 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 org.devtcg.five.Constants; import org.devtcg.five.R; import org.devtcg.five.provider.Five; import org.devtcg.five.service.IPlaylistBufferListener; import org.devtcg.five.service.IPlaylistDownloadListener; import org.devtcg.five.service.IPlaylistMoveListener; import org.devtcg.five.util.PlaylistServiceActivity; import org.devtcg.five.util.Song; import org.devtcg.five.widget.PlayerControls; import android.content.Context; import android.content.Intent; import android.media.AudioManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; public class Player extends PlaylistServiceActivity { private static final String TAG = "Playlist"; private ProgressHandler mHandler = new ProgressHandler(); private TextView mArtist; private TextView mAlbum; private TextView mSong; private PlayerControls mControlsView; private boolean mPaused = false; private Song mSongPlaying; /* Flag used to determine if we have exercised an optimization potential * given by the supplied Intent's extras. */ private boolean mHintedOpt = false; private static final int MENU_SET_RANDOM = Menu.FIRST; private static final int MENU_MORE_BY_ARTIST = Menu.FIRST + 1; private static final int MENU_RETURN_LIBRARY = Menu.FIRST + 2; private static final int MENU_ARTIST_BIO = Menu.FIRST + 3; private static final int MENU_PLAYQUEUE = Menu.FIRST + 4; public static void show(Context context) { context.startActivity(new Intent(context, Player.class)); } @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setVolumeControlStream(AudioManager.STREAM_MUSIC); loadWithHint(getIntent()); } @Override protected void onNewIntent(Intent intent) { loadWithHint(intent); } private void loadWithHint(Intent intent) { Bundle extras = getIntent().getExtras(); if (extras != null) { mHintedOpt = true; showUI(true); setNowPlaying(extras); mControlsView.showProgressControls(); } } @Override protected void onInitUI() { setContentView(R.layout.player); mArtist = (TextView)findViewById(R.id.artist_name); mAlbum = (TextView)findViewById(R.id.album_name); mSong = (TextView)findViewById(R.id.song_name); mControlsView = (PlayerControls)findViewById(R.id.player_controls); mControlsView.setOnControlClickListener(mControlClick); mControlsView.getSeekBar().setOnSeekBarChangeListener(mSeeked); } @Override protected void onAttached() { super.onAttached(); try { mService.registerOnBufferingListener(mServiceBufferListener); mService.registerOnMoveListener(mServiceMoveListener); mService.registerOnDownloadListener(mServiceDownloadListener); if (mService.isPlaying() == false) setNotPlaying(); else { int pos = mService.getPosition(); long songId = mService.getSongAt(pos); if (mSongPlaying == null || mSongPlaying.id != songId) { if (mHintedOpt == true) Log.w(TAG, "Hinted Intent was wrong!"); setNowPlaying(songId, pos); } setPausedState(mService.isPaused()); } } catch (RemoteException e) {} } @Override protected void onDetached() { super.onDetached(); try { mService.unregisterOnBufferingListener(mServiceBufferListener); mService.unregisterOnMoveListener(mServiceMoveListener); mService.unregisterOnDownloadListener(mServiceDownloadListener); } catch (RemoteException e) {} } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); // menu.add(0, MENU_SET_RANDOM, Menu.NONE, "Random") // .setCheckable(true) // .setIcon(R.drawable.ic_menu_shuffle) menu.add(0, MENU_MORE_BY_ARTIST, Menu.NONE, "More By Artist") .setIcon(R.drawable.ic_menu_add); menu.add(0, MENU_RETURN_LIBRARY, Menu.NONE, R.string.return_library) .setIcon(R.drawable.ic_menu_music_library); menu.add(0, MENU_ARTIST_BIO, Menu.NONE, "Artist Info") .setIcon(R.drawable.ic_menu_artist_info); menu.add(0, MENU_PLAYQUEUE, Menu.NONE, "Play queue") .setIcon(R.drawable.ic_menu_playqueue); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.findItem(MENU_MORE_BY_ARTIST).setVisible(mSongPlaying != null); menu.findItem(MENU_ARTIST_BIO).setVisible(mSongPlaying != null); return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MENU_SET_RANDOM: Toast.makeText(this,"Not implemented", Toast.LENGTH_SHORT).show(); return true; case MENU_MORE_BY_ARTIST: chooseArtist(); return true; case MENU_RETURN_LIBRARY: Main.show(this); return true; case MENU_ARTIST_BIO: Toast.makeText(this,"Not implemented", Toast.LENGTH_SHORT).show(); return true; case MENU_PLAYQUEUE: SongList.actionOpenPlayQueue(this); return true; } return false; } /* TODO: Generalize a way to re-use existing chooser building * functions (in this case, the one in ArtistList.java). */ private void chooseArtist() { if (mSongPlaying == null) return; Intent chosen = new Intent(); chosen.setData(Five.Music.Artists.CONTENT_URI.buildUpon() .appendEncodedPath(String.valueOf(mSongPlaying.artistId)).build()); chosen.putExtra(Constants.EXTRA_ARTIST_NAME, mSongPlaying.artist); chosen.setAction(Intent.ACTION_VIEW); chosen.setClass(this, ArtistAlbumList.class); startActivity(chosen); } private class ProgressHandler extends Handler { private static final int MSG_DOWNLOAD_PROGRESS = 1; private static final int MSG_PLAYBACK_PROGRESS = 2; public void handleMessage(Message msg) { switch (msg.what) { case MSG_DOWNLOAD_PROGRESS: if (mSongPlaying != null && mSongPlaying.id == (Long)msg.obj) mControlsView.getSeekBar().setSecondaryProgress(msg.arg1); break; case MSG_PLAYBACK_PROGRESS: try { if (mService != null && mService.isOutputting() == true) { long pos = mService.tell(); long dur = mService.getSongDuration(); int progress = (int)(((float)pos / (float)dur) * 100f); mControlsView.setTrackPosition((int)(pos / 1000), (int)(dur / 1000)); mControlsView.getSeekBar().setProgress(progress); } } catch (RemoteException e) {} sendMessageDelayed(obtainMessage(MSG_PLAYBACK_PROGRESS), 1000); break; default: super.handleMessage(msg); } } public void startPlaybackMonitoring(boolean updateNow) { removeMessages(MSG_PLAYBACK_PROGRESS); Message msg = obtainMessage(MSG_PLAYBACK_PROGRESS); if (updateNow == true) handleMessage(msg); else sendMessage(msg); } public void stopPlaybackMonitoring() { removeMessages(MSG_PLAYBACK_PROGRESS); } public void sendDownloadProgress(long songId, int progress) { sendMessage(obtainMessage(MSG_DOWNLOAD_PROGRESS, progress, -1, (Long)songId)); } } private final SeekBar.OnSeekBarChangeListener mSeeked = new SeekBar.OnSeekBarChangeListener() { public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) { if (fromTouch == false) return; try { long dur = mService.getSongDuration(); long target = (long)((progress / 100f) * dur); mService.seek(target); } catch (RemoteException e) {} } public void onStartTrackingTouch(SeekBar seekBar) {} public void onStopTrackingTouch(SeekBar seekBar) {} }; private final OnClickListener mControlClick = new OnClickListener() { public void onClick(View v) { switch (v.getId()) { case R.id.control_next: doMove(1); break; case R.id.control_prev: doMove(-1); break; case R.id.control_pause: doPauseToggle(); break; } } }; private void doMove(int direction) { if (mService == null) return; int pos; try { if (direction > 0) pos = mService.next(); else pos = mService.previous(); } catch (RemoteException e) { return; } if (pos == -1) { Toast.makeText(Player.this, "End of playlist", Toast.LENGTH_SHORT).show(); } } private void doPauseToggle() { if (mService == null) return; try { if (mPaused == true) { if (mService.isPaused() == true) mService.unpause(); else mService.play(); setPausedState(false); } else { mService.pause(); setPausedState(true); } } catch (RemoteException e) { finish(); } } private void setNotPlaying() { mSongPlaying = null; Log.d(TAG, "*NOT PLAYING*"); mSong.setText("End of Playlist"); mArtist.setText(""); mAlbum.setText(""); mControlsView.setNotPlaying(); /* Not really, we just want the mechanics to restart playback. */ setPausedState(true); mHandler.stopPlaybackMonitoring(); } private void setNowPlaying(Song song, int pos, int len) { Log.d(TAG, "setNowPlaying(" + song.id + "; pos=" + pos + "; len=" + len + ")"); if (mSongPlaying == null || mSongPlaying.albumId != song.albumId) { Log.i(TAG, "Setting a new album for albumId=" + song.albumId); Uri coverNow = (mSongPlaying != null ? mSongPlaying.albumCoverBig : null); if (song.albumCoverBig == null) mControlsView.setAlbumCover(R.drawable.lastfm_cover); else { if (coverNow == null || coverNow.equals(song.albumCoverBig) == false) mControlsView.setAlbumCover(song.albumCoverBig); } } mSongPlaying = song; mSong.setText(song.title); mArtist.setText(song.artist); mAlbum.setText(song.album); mControlsView.setPlaylistPosition(pos + 1, len); mControlsView.setTrackPosition(0, (int)song.length); mHandler.startPlaybackMonitoring(true); } private void setNowPlaying(long songId, int pos) throws RemoteException { setNowPlaying(new Song(this, songId), pos, mService.getPlaylistLength()); } /* Special way to startup where we are handed our initial UI state so * we can avoid an expensive query. */ private void setNowPlaying(Bundle e) { Song song = new Song(); song.artistId = e.getLong(Constants.EXTRA_ARTIST_ID); song.artist = e.getString(Constants.EXTRA_ARTIST_NAME); song.albumId = e.getLong(Constants.EXTRA_ALBUM_ID); song.album = e.getString(Constants.EXTRA_ALBUM_NAME); String artwork = e.getString(Constants.EXTRA_ALBUM_ARTWORK_LARGE); if (artwork != null) song.albumCoverBig = Uri.parse(artwork); song.id = e.getLong(Constants.EXTRA_SONG_ID); song.title = e.getString(Constants.EXTRA_SONG_TITLE); song.length = e.getLong(Constants.EXTRA_SONG_LENGTH); setNowPlaying(song, e.getInt(Constants.EXTRA_PLAYLIST_POSITION), e.getInt(Constants.EXTRA_PLAYLIST_LENGTH)); } private void setPausedState(boolean paused) { if (mPaused == paused) return; int resid = (paused == true) ? android.R.drawable.ic_media_play : android.R.drawable.ic_media_pause; mControlsView.getPauseButton().setImageResource(resid); mPaused = paused; } private final IPlaylistBufferListener.Stub mServiceBufferListener = new IPlaylistBufferListener.Stub() { public void onBufferingUpdate(long songId, final int bufferPercent) throws RemoteException { mHandler.post(new Runnable() { public void run() { mControlsView.setBufferPercent(bufferPercent); /* If the buffer is empty, show the progress bar until the * download begins again or the player aborts. Otherwise, * start the timeout to automatically hide the progress * controls */ if (bufferPercent == 0) mControlsView.showProgressControls(0); else if (bufferPercent == 100) { /* * This reads funny, I get that. We're really trying to * make sure the auto-hide timer is set up. */ if (mControlsView.progressControlsAreShown()) mControlsView.showProgressControls(); } } }); } }; private final IPlaylistMoveListener.Stub mServiceMoveListener = new IPlaylistMoveListener.Stub() { public void onAdvance() throws RemoteException { } public void onJump(final int pos) throws RemoteException { final long songId = mService.getSongAt(pos); assert songId >= 0; mHandler.post(new Runnable() { public void run() { try { setNowPlaying(songId, pos); } catch (RemoteException e) {} /* Jump implicitly unpauses. */ setPausedState(false); } }); } public void onPause() throws RemoteException { mHandler.post(new Runnable() { public void run() { if (mPaused == true) return; setPausedState(true); } }); } public void onPlay() throws RemoteException { final int pos = mService.getPosition(); final long songId = mService.getSongAt(pos); assert songId >= 0; mHandler.post(new Runnable() { public void run() { try { setNowPlaying(songId, pos); } catch (RemoteException e) {} setPausedState(false); } }); } public void onStop() throws RemoteException { mHandler.post(new Runnable() { public void run() { setNotPlaying(); } }); } public void onSeek(long pos) throws RemoteException { } public void onUnpause() throws RemoteException { mHandler.post(new Runnable() { public void run() { if (mPaused == false) return; setPausedState(false); } }); } }; private final IPlaylistDownloadListener.Stub mServiceDownloadListener = new IPlaylistDownloadListener.Stub() { public void onDownloadBegin(long songId) throws RemoteException {} public void onDownloadCancel(long songId) throws RemoteException {} public void onDownloadError(long songId, final String err) throws RemoteException { Log.i(TAG, "Error downloading songId=" + songId + ": " + err); mHandler.post(new Runnable() { public void run() { Toast.makeText(Player.this, "Aww shit: " + err, Toast.LENGTH_LONG).show(); } }); } public void onDownloadFinish(final long songId) throws RemoteException { Log.i(TAG, "Download finished for songId=" + songId); mHandler.post(new Runnable() { public void run() { if (mSongPlaying != null && mSongPlaying.id == songId) mControlsView.getSeekBar().setSecondaryProgress(0); } }); } public void onDownloadProgressUpdate(long songId, int percent) throws RemoteException { mHandler.sendDownloadProgress(songId, percent); } }; }