diff --git a/res/drawable/next.xml b/res/drawable/next.xml new file mode 100644 index 00000000..b1a1746b --- /dev/null +++ b/res/drawable/next.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/next_focused.png b/res/drawable/next_focused.png new file mode 100644 index 00000000..9d654d60 Binary files /dev/null and b/res/drawable/next_focused.png differ diff --git a/res/drawable/next_normal.png b/res/drawable/next_normal.png new file mode 100644 index 00000000..2552f4ed Binary files /dev/null and b/res/drawable/next_normal.png differ diff --git a/res/drawable/pause.xml b/res/drawable/pause.xml new file mode 100644 index 00000000..8b279211 --- /dev/null +++ b/res/drawable/pause.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/pause_focused.png b/res/drawable/pause_focused.png new file mode 100644 index 00000000..c0730b0a Binary files /dev/null and b/res/drawable/pause_focused.png differ diff --git a/res/drawable/pause_normal.png b/res/drawable/pause_normal.png new file mode 100644 index 00000000..61df5c86 Binary files /dev/null and b/res/drawable/pause_normal.png differ diff --git a/res/drawable/play.xml b/res/drawable/play.xml new file mode 100644 index 00000000..1a04f654 --- /dev/null +++ b/res/drawable/play.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/play_focused.png b/res/drawable/play_focused.png new file mode 100644 index 00000000..233a5655 Binary files /dev/null and b/res/drawable/play_focused.png differ diff --git a/res/drawable/play_normal.png b/res/drawable/play_normal.png new file mode 100644 index 00000000..2092bd57 Binary files /dev/null and b/res/drawable/play_normal.png differ diff --git a/res/drawable/previous.xml b/res/drawable/previous.xml new file mode 100644 index 00000000..12d33989 --- /dev/null +++ b/res/drawable/previous.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/previous_focused.png b/res/drawable/previous_focused.png new file mode 100644 index 00000000..ba6a3bef Binary files /dev/null and b/res/drawable/previous_focused.png differ diff --git a/res/drawable/previous_normal.png b/res/drawable/previous_normal.png new file mode 100644 index 00000000..05eba718 Binary files /dev/null and b/res/drawable/previous_normal.png differ diff --git a/res/layout/nowplaying.xml b/res/layout/nowplaying.xml new file mode 100644 index 00000000..61485ad7 --- /dev/null +++ b/res/layout/nowplaying.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/org/kreed/tumult/CoverView.java b/src/org/kreed/tumult/CoverView.java index e1642a6a..5276db3f 100755 --- a/src/org/kreed/tumult/CoverView.java +++ b/src/org/kreed/tumult/CoverView.java @@ -8,6 +8,7 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; +import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; @@ -32,17 +33,17 @@ public class CoverView extends View { public interface CoverViewWatcher { public void next(); public void previous(); - public void togglePlayback(); + public void clicked(); } - public CoverView(Context context) + public CoverView(Context context, AttributeSet attributes) { - super(context); + super(context, attributes); mScroller = new Scroller(context); } - public void setCoverSwapListener(CoverViewWatcher listener) + public void setWatcher(CoverViewWatcher listener) { mListener = listener; } @@ -254,7 +255,7 @@ public class CoverView extends View { break; case MotionEvent.ACTION_UP: if (Math.abs(mStartX - x) + Math.abs(mStartY - ev.getY()) < 10) { - mListener.togglePlayback(); + mListener.clicked(); } else { VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000); diff --git a/src/org/kreed/tumult/IMusicPlayerWatcher.aidl b/src/org/kreed/tumult/IMusicPlayerWatcher.aidl index f81d0232..0a060931 100644 --- a/src/org/kreed/tumult/IMusicPlayerWatcher.aidl +++ b/src/org/kreed/tumult/IMusicPlayerWatcher.aidl @@ -6,4 +6,5 @@ oneway interface IMusicPlayerWatcher { void previousSong(in Song playingSong, in Song nextForwardSong); void nextSong(in Song playingSong, in Song nextBackwardSong); void stateChanged(in int oldState, in int newState); + void mediaLengthChanged(in long startTime, in int duration); } \ No newline at end of file diff --git a/src/org/kreed/tumult/IPlaybackService.aidl b/src/org/kreed/tumult/IPlaybackService.aidl index fc36e11f..89fcee56 100644 --- a/src/org/kreed/tumult/IPlaybackService.aidl +++ b/src/org/kreed/tumult/IPlaybackService.aidl @@ -7,8 +7,12 @@ interface IPlaybackService { void registerWatcher(IMusicPlayerWatcher watcher); Song[] getCurrentSongs(); + int getState(); + long getStartTime(); + int getDuration(); void previousSong(); void togglePlayback(); void nextSong(); + void seekToProgress(int progress); } \ No newline at end of file diff --git a/src/org/kreed/tumult/MusicPlayer.java b/src/org/kreed/tumult/MusicPlayer.java index 86dd4d48..129796c5 100644 --- a/src/org/kreed/tumult/MusicPlayer.java +++ b/src/org/kreed/tumult/MusicPlayer.java @@ -29,12 +29,32 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener, public static final int STATE_NORMAL = 0; public static final int STATE_NO_MEDIA = 1; + public static final int STATE_PLAYING = 2; public IPlaybackService.Stub mBinder = new IPlaybackService.Stub() { public Song[] getCurrentSongs() { return new Song[] { getSong(-1), getSong(0), getSong(1) }; } + + public int getState() + { + return mState; + } + + public long getStartTime() + { + if (mMediaPlayer == null) + return 0; + return MusicPlayer.this.getStartTime(); + } + + public int getDuration() + { + if (mMediaPlayer == null) + return 0; + return mMediaPlayer.getDuration(); + } public void nextSong() { @@ -62,6 +82,16 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener, if (watcher != null) mWatchers.register(watcher); } + + public void seekToProgress(int progress) + { + if (mMediaPlayer == null || !mMediaPlayer.isPlaying()) + return; + + long position = (long)mMediaPlayer.getDuration() * progress / 1000; + mMediaPlayer.seekTo((int)position); + mediaLengthChanged(); + } }; public void queueSong(int songId) @@ -201,6 +231,9 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener, public void setState(int state) { + if (mState == state) + return; + int oldState = mState; mState = state; @@ -244,12 +277,15 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener, notification.contentIntent = PendingIntent.getActivity(mService, 0, intent, 0); mService.startForegroundCompat(NOTIFICATION_ID, notification); + + setState(STATE_PLAYING); } private void pause() { mMediaPlayer.pause(); mService.stopForegroundCompat(NOTIFICATION_ID); + setState(STATE_NORMAL); } private void setPlaying(boolean play) @@ -291,6 +327,8 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener, Log.e("Tumult", "IOException", e); } + mediaLengthChanged(); + getSong(+2); while (mCurrentSong > 15) { @@ -298,6 +336,28 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener, --mCurrentSong; } } + + private long getStartTime() + { + int position = mMediaPlayer.getCurrentPosition(); + return System.currentTimeMillis() - position; + } + + private void mediaLengthChanged() + { + long start = getStartTime(); + int duration = mMediaPlayer.getDuration(); + + int i = mWatchers.beginBroadcast(); + while (--i != -1) { + try { + mWatchers.getBroadcastItem(i).mediaLengthChanged(start, duration); + } catch (RemoteException e) { + // Null elements will be removed automatically + } + } + mWatchers.finishBroadcast(); + } public void onCompletion(MediaPlayer player) { @@ -306,7 +366,7 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener, private Song randomSong() { - return new Song(mSongs[mRandom.nextInt(mSongs.length)]); + return new Song(mSongs[mRandom.nextInt(mSongs.length)]); } private synchronized Song getSong(int delta) diff --git a/src/org/kreed/tumult/NowPlayingActivity.java b/src/org/kreed/tumult/NowPlayingActivity.java index c60d80c2..145abdb0 100644 --- a/src/org/kreed/tumult/NowPlayingActivity.java +++ b/src/org/kreed/tumult/NowPlayingActivity.java @@ -8,22 +8,41 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; +import android.os.Handler; import android.os.IBinder; +import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.view.Gravity; +import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.SeekBar; import android.widget.TextView; -public class NowPlayingActivity extends Activity implements CoverViewWatcher, ServiceConnection { +public class NowPlayingActivity extends Activity implements CoverViewWatcher, ServiceConnection, View.OnClickListener, SeekBar.OnSeekBarChangeListener { private IPlaybackService mService; + + private ViewGroup mLayout; private CoverView mCoverView; private LinearLayout mMessageBox; + private View mControls; + + private ImageButton mPreviousButton; + private ImageButton mPlayPauseButton; + private ImageButton mNextButton; + private SeekBar mSeekBar; + private TextView mSeekText; + + private int mState; + private long mStartTime; + private int mDuration; + private boolean mSeekBarTracking; - private static final int MENU_PREVIOUS = 0; - private static final int MENU_NEXT = 1; private static final int MENU_PREFS = 2; private static final int MENU_QUEUE = 3; @@ -32,18 +51,40 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se { super.onCreate(icicle); - mCoverView = new CoverView(this); - mCoverView.setCoverSwapListener(this);; - setContentView(mCoverView); -// Bundle extras = getIntent().getExtras(); + setContentView(R.layout.nowplaying); + + mCoverView = (CoverView)findViewById(R.id.cover_view); + mCoverView.setWatcher(this); + + mLayout = (ViewGroup)mCoverView.getParent(); + + mControls = findViewById(R.id.controls); + + mPreviousButton = (ImageButton)findViewById(R.id.previous); + mPreviousButton.setOnClickListener(this); + mPlayPauseButton = (ImageButton)findViewById(R.id.play_pause); + mPlayPauseButton.setOnClickListener(this); + mNextButton = (ImageButton)findViewById(R.id.next); + mNextButton.setOnClickListener(this); + + mSeekText = (TextView)findViewById(R.id.seek_text); + mSeekBar = (SeekBar)findViewById(R.id.seek_bar); + mSeekBar.setMax(1000); + mSeekBar.setOnSeekBarChangeListener(this); } public void setState(int state) { + mState = state; + switch (state) { case MusicPlayer.STATE_NORMAL: - setContentView(mCoverView); - mMessageBox = null; + case MusicPlayer.STATE_PLAYING: + if (mMessageBox != null) { + mLayout.removeView(mMessageBox); + mMessageBox = null; + } + mPlayPauseButton.setImageResource(state == MusicPlayer.STATE_PLAYING ? R.drawable.pause : R.drawable.play); break; case MusicPlayer.STATE_NO_MEDIA: mMessageBox = new LinearLayout(this); @@ -51,11 +92,11 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se TextView text = new TextView(this); text.setText("No songs found on your device."); text.setGravity(Gravity.CENTER); - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.FILL_PARENT); layoutParams.gravity = Gravity.CENTER; text.setLayoutParams(layoutParams); mMessageBox.addView(text); - setContentView(mMessageBox); + mLayout.addView(mMessageBox); break; } } @@ -98,6 +139,7 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se try { mService.registerWatcher(mWatcher); refreshSongs(); + setState(mService.getState()); } catch (RemoteException e) { Log.i("Tumult", "Failed to initialize connection to playback service", e); } @@ -147,6 +189,12 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se } }); } + + public void mediaLengthChanged(long startTime, int duration) + { + mStartTime = startTime; + mDuration = duration; + } }; public void next() @@ -167,7 +215,7 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se } } - public void togglePlayback() + private void togglePlayback() { try { mService.togglePlayback(); @@ -178,8 +226,6 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se @Override public boolean onCreateOptionsMenu(Menu menu) { - menu.add(0, MENU_PREVIOUS, 0, "Previous"); - menu.add(0, MENU_NEXT, 0, "Next"); menu.add(0, MENU_PREFS, 0, "Preferences"); menu.add(0, MENU_QUEUE, 0, "Add to Queue"); return true; @@ -188,33 +234,139 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se @Override public boolean onOptionsItemSelected(final MenuItem item) { - new Thread(new Runnable() { - public void run() - { - switch (item.getItemId()) { - case MENU_PREVIOUS: - previous(); - break; - case MENU_NEXT: - next(); - break; - case MENU_PREFS: - startActivity(new Intent(NowPlayingActivity.this, PreferencesActivity.class)); - break; - case MENU_QUEUE: - onSearchRequested(); - break; - } - } - }).start(); + switch (item.getItemId()) { + case MENU_PREFS: + startActivity(new Intent(this, PreferencesActivity.class)); + break; + case MENU_QUEUE: + onSearchRequested(); + break; + } return true; } - + @Override public boolean onSearchRequested() { startActivity(new Intent(this, SongSelector.class)); return false; } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) + { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + clicked(); + return true; + } + + return false; + } + + public void clicked() + { + mControls.setVisibility(View.VISIBLE); + + if (mStartTime == 0) { + try { + mStartTime = mService.getStartTime(); + mDuration = mService.getDuration(); + } catch (RemoteException e) { + return; + } + } + + updateProgress(); + sendHideMessage(); + } + + private String stringForTime(int ms) + { + int seconds = ms / 1000; + + int hours = seconds / 3600; + seconds -= hours * 3600; + int minutes = seconds / 60; + seconds -= minutes * 60; + + if (hours > 0) + return String.format("%d:%02d:%02d", hours, minutes, seconds); + else + return String.format("%02d:%02d", minutes, seconds); + } + + private void updateProgress() + { + if (mState != MusicPlayer.STATE_PLAYING || mControls.getVisibility() != View.VISIBLE) + return; + + long position = System.currentTimeMillis() - mStartTime; + if (!mSeekBarTracking) + mSeekBar.setProgress((int)(1000 * position / mDuration)); + mSeekText.setText(stringForTime((int)position) + " / " + stringForTime(mDuration)); + + long next = 1000 - position % 1000; + mHandler.sendMessageDelayed(mHandler.obtainMessage(UPDATE_PROGRESS), next); + } + + private void sendHideMessage() + { + Message message = mHandler.obtainMessage(HIDE); + mHandler.removeMessages(HIDE); + mHandler.sendMessageDelayed(message, 3000); + } + + public void onClick(View view) + { + sendHideMessage(); + + if (view == mNextButton) { + next(); + } else if (view == mPreviousButton) { + previous(); + } else if (view == mPlayPauseButton) { + togglePlayback(); + } + } + + private static final int HIDE = 0; + private static final int UPDATE_PROGRESS = 1; + + private Handler mHandler = new Handler() { + public void handleMessage(Message message) { + switch (message.what) { + case HIDE: + mControls.setVisibility(View.GONE); + break; + case UPDATE_PROGRESS: + updateProgress(); + break; + } + } + }; + + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) + { + if (fromUser) { + try { + mService.seekToProgress(progress); + } catch (RemoteException e) { + } + } + } + + public void onStartTrackingTouch(SeekBar seekBar) + { + mHandler.removeMessages(HIDE); + mSeekBarTracking = true; + } + + public void onStopTrackingTouch(SeekBar seekBar) + { + sendHideMessage(); + mSeekBarTracking = false; + } } \ No newline at end of file