2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.music;
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.KeyguardManager;
22 import android.app.SearchManager;
23 import android.content.BroadcastReceiver;
24 import android.content.ComponentName;
25 import android.content.ContentResolver;
26 import android.content.ContentUris;
27 import android.content.Context;
28 import android.content.DialogInterface;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.ServiceConnection;
32 import android.content.res.Configuration;
33 import android.database.Cursor;
34 import android.graphics.Bitmap;
35 import android.media.AudioManager;
36 import android.net.Uri;
37 import android.os.Bundle;
38 import android.os.Handler;
39 import android.os.IBinder;
40 import android.os.Looper;
41 import android.os.Message;
42 import android.os.RemoteException;
43 import android.os.SystemClock;
44 import android.provider.MediaStore;
45 import android.text.Layout;
46 import android.text.TextUtils.TruncateAt;
47 import android.util.Log;
48 import android.view.KeyEvent;
49 import android.view.Menu;
50 import android.view.MenuItem;
51 import android.view.MotionEvent;
52 import android.view.SubMenu;
53 import android.view.View;
54 import android.view.ViewConfiguration;
55 import android.view.Window;
56 import android.widget.ImageButton;
57 import android.widget.ImageView;
58 import android.widget.ProgressBar;
59 import android.widget.SeekBar;
60 import android.widget.TextView;
61 import android.widget.Toast;
62 import android.widget.SeekBar.OnSeekBarChangeListener;
65 public class MediaPlaybackActivity extends Activity implements MusicUtils.Defs,
66 View.OnTouchListener, View.OnLongClickListener
68 private static final int USE_AS_RINGTONE = CHILD_MENU_BASE;
70 private boolean mOneShot = false;
71 private boolean mSeeking = false;
72 private boolean mDeviceHasDpad;
73 private long mStartSeekPos = 0;
74 private long mLastSeekEventTime;
75 private IMediaPlaybackService mService = null;
76 private RepeatingImageButton mPrevButton;
77 private ImageButton mPauseButton;
78 private RepeatingImageButton mNextButton;
79 private ImageButton mRepeatButton;
80 private ImageButton mShuffleButton;
81 private ImageButton mQueueButton;
82 private Worker mAlbumArtWorker;
83 private AlbumArtHandler mAlbumArtHandler;
85 private int mTouchSlop;
87 public MediaPlaybackActivity()
91 /** Called when the activity is first created. */
93 public void onCreate(Bundle icicle)
95 super.onCreate(icicle);
96 setVolumeControlStream(AudioManager.STREAM_MUSIC);
98 mAlbumArtWorker = new Worker("album art worker");
99 mAlbumArtHandler = new AlbumArtHandler(mAlbumArtWorker.getLooper());
101 requestWindowFeature(Window.FEATURE_NO_TITLE);
102 setContentView(R.layout.audio_player);
104 mCurrentTime = (TextView) findViewById(R.id.currenttime);
105 mTotalTime = (TextView) findViewById(R.id.totaltime);
106 mProgress = (ProgressBar) findViewById(android.R.id.progress);
107 mAlbum = (ImageView) findViewById(R.id.album);
108 mArtistName = (TextView) findViewById(R.id.artistname);
109 mAlbumName = (TextView) findViewById(R.id.albumname);
110 mTrackName = (TextView) findViewById(R.id.trackname);
112 View v = (View)mArtistName.getParent();
113 v.setOnTouchListener(this);
114 v.setOnLongClickListener(this);
116 v = (View)mAlbumName.getParent();
117 v.setOnTouchListener(this);
118 v.setOnLongClickListener(this);
120 v = (View)mTrackName.getParent();
121 v.setOnTouchListener(this);
122 v.setOnLongClickListener(this);
124 mPrevButton = (RepeatingImageButton) findViewById(R.id.prev);
125 mPrevButton.setOnClickListener(mPrevListener);
126 mPrevButton.setRepeatListener(mRewListener, 260);
127 mPauseButton = (ImageButton) findViewById(R.id.pause);
128 mPauseButton.requestFocus();
129 mPauseButton.setOnClickListener(mPauseListener);
130 mNextButton = (RepeatingImageButton) findViewById(R.id.next);
131 mNextButton.setOnClickListener(mNextListener);
132 mNextButton.setRepeatListener(mFfwdListener, 260);
135 mDeviceHasDpad = (getResources().getConfiguration().navigation ==
136 Configuration.NAVIGATION_DPAD);
138 mQueueButton = (ImageButton) findViewById(R.id.curplaylist);
139 mQueueButton.setOnClickListener(mQueueListener);
140 mShuffleButton = ((ImageButton) findViewById(R.id.shuffle));
141 mShuffleButton.setOnClickListener(mShuffleListener);
142 mRepeatButton = ((ImageButton) findViewById(R.id.repeat));
143 mRepeatButton.setOnClickListener(mRepeatListener);
145 if (mProgress instanceof SeekBar) {
146 SeekBar seeker = (SeekBar) mProgress;
147 seeker.setOnSeekBarChangeListener(mSeekListener);
149 mProgress.setMax(1000);
151 if (icicle != null) {
152 mOneShot = icicle.getBoolean("oneshot");
154 mOneShot = getIntent().getBooleanExtra("oneshot", false);
157 mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
164 boolean mDraggingLabel = false;
166 TextView textViewForContainer(View v) {
167 View vv = v.findViewById(R.id.artistname);
168 if (vv != null) return (TextView) vv;
169 vv = v.findViewById(R.id.albumname);
170 if (vv != null) return (TextView) vv;
171 vv = v.findViewById(R.id.trackname);
172 if (vv != null) return (TextView) vv;
176 public boolean onTouch(View v, MotionEvent event) {
177 int action = event.getAction();
178 TextView tv = textViewForContainer(v);
182 if (action == MotionEvent.ACTION_DOWN) {
183 v.setBackgroundColor(0xff606060);
184 mInitialX = mLastX = (int) event.getX();
185 mDraggingLabel = false;
186 } else if (action == MotionEvent.ACTION_UP ||
187 action == MotionEvent.ACTION_CANCEL) {
188 v.setBackgroundColor(0);
189 if (mDraggingLabel) {
190 Message msg = mLabelScroller.obtainMessage(0, tv);
191 mLabelScroller.sendMessageDelayed(msg, 1000);
193 } else if (action == MotionEvent.ACTION_MOVE) {
194 if (mDraggingLabel) {
195 int scrollx = tv.getScrollX();
196 int x = (int) event.getX();
197 int delta = mLastX - x;
201 if (scrollx > mTextWidth) {
202 // scrolled the text completely off the view to the left
203 scrollx -= mTextWidth;
204 scrollx -= mViewWidth;
206 if (scrollx < -mViewWidth) {
207 // scrolled the text completely off the view to the right
208 scrollx += mViewWidth;
209 scrollx += mTextWidth;
211 tv.scrollTo(scrollx, 0);
215 int delta = mInitialX - (int) event.getX();
216 if (Math.abs(delta) > mTouchSlop) {
218 mLabelScroller.removeMessages(0, tv);
220 // Only turn ellipsizing off when it's not already off, because it
221 // causes the scroll position to be reset to 0.
222 if (tv.getEllipsize() != null) {
223 tv.setEllipsize(null);
225 Layout ll = tv.getLayout();
226 // layout might be null if the text just changed, or ellipsizing
227 // was just turned off
231 // get the non-ellipsized line width, to determine whether scrolling
232 // should even be allowed
233 mTextWidth = (int) tv.getLayout().getLineWidth(0);
234 mViewWidth = tv.getWidth();
235 if (mViewWidth > mTextWidth) {
236 tv.setEllipsize(TruncateAt.END);
240 mDraggingLabel = true;
241 tv.setHorizontalFadingEdgeEnabled(true);
249 Handler mLabelScroller = new Handler() {
251 public void handleMessage(Message msg) {
252 TextView tv = (TextView) msg.obj;
253 int x = tv.getScrollX();
257 tv.setEllipsize(TruncateAt.END);
259 Message newmsg = obtainMessage(0, tv);
260 mLabelScroller.sendMessageDelayed(newmsg, 15);
265 public boolean onLongClick(View view) {
267 CharSequence title = null;
276 artist = mService.getArtistName();
277 album = mService.getAlbumName();
278 song = mService.getTrackName();
279 audioid = mService.getAudioId();
280 } catch (RemoteException ex) {
282 } catch (NullPointerException ex) {
283 // we might not actually have the service yet
287 if (MediaStore.UNKNOWN_STRING.equals(album) &&
288 MediaStore.UNKNOWN_STRING.equals(artist) &&
290 song.startsWith("recording")) {
299 Cursor c = MusicUtils.query(this,
300 ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, audioid),
301 new String[] {MediaStore.Audio.Media.IS_MUSIC}, null, null, null);
302 boolean ismusic = true;
304 if (c.moveToFirst()) {
305 ismusic = c.getInt(0) != 0;
313 boolean knownartist =
314 (artist != null) && !MediaStore.UNKNOWN_STRING.equals(artist);
317 (album != null) && !MediaStore.UNKNOWN_STRING.equals(album);
319 if (knownartist && view.equals(mArtistName.getParent())) {
322 mime = MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE;
323 } else if (knownalbum && view.equals(mAlbumName.getParent())) {
326 query = artist + " " + album;
330 mime = MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE;
331 } else if (view.equals(mTrackName.getParent()) || !knownartist || !knownalbum) {
332 if ((song == null) || MediaStore.UNKNOWN_STRING.equals(song)) {
333 // A popup of the form "Search for null/'' using ..." is pretty
334 // unhelpful, plus, we won't find any way to buy it anyway.
340 query = artist + " " + song;
344 mime = "audio/*"; // the specific type doesn't matter, so don't bother retrieving it
346 throw new RuntimeException("shouldn't be here");
348 title = getString(R.string.mediasearch, title);
350 Intent i = new Intent();
351 i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
352 i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
353 i.putExtra(SearchManager.QUERY, query);
355 i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, artist);
358 i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, album);
360 i.putExtra(MediaStore.EXTRA_MEDIA_TITLE, song);
361 i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, mime);
363 startActivity(Intent.createChooser(i, title));
367 private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
368 public void onStartTrackingTouch(SeekBar bar) {
369 mLastSeekEventTime = 0;
372 public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
373 if (!fromuser || (mService == null)) return;
374 long now = SystemClock.elapsedRealtime();
375 if ((now - mLastSeekEventTime) > 250) {
376 mLastSeekEventTime = now;
377 mPosOverride = mDuration * progress / 1000;
379 mService.seek(mPosOverride);
380 } catch (RemoteException ex) {
383 // trackball event, allow progress updates
390 public void onStopTrackingTouch(SeekBar bar) {
396 private View.OnClickListener mQueueListener = new View.OnClickListener() {
397 public void onClick(View v) {
399 new Intent(Intent.ACTION_EDIT)
400 .setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track")
401 .putExtra("playlist", "nowplaying")
406 private View.OnClickListener mShuffleListener = new View.OnClickListener() {
407 public void onClick(View v) {
412 private View.OnClickListener mRepeatListener = new View.OnClickListener() {
413 public void onClick(View v) {
418 private View.OnClickListener mPauseListener = new View.OnClickListener() {
419 public void onClick(View v) {
424 private View.OnClickListener mPrevListener = new View.OnClickListener() {
425 public void onClick(View v) {
426 if (mService == null) return;
428 if (mService.position() < 2000) {
434 } catch (RemoteException ex) {
439 private View.OnClickListener mNextListener = new View.OnClickListener() {
440 public void onClick(View v) {
441 if (mService == null) return;
444 } catch (RemoteException ex) {
449 private RepeatingImageButton.RepeatListener mRewListener =
450 new RepeatingImageButton.RepeatListener() {
451 public void onRepeat(View v, long howlong, int repcnt) {
452 scanBackward(repcnt, howlong);
456 private RepeatingImageButton.RepeatListener mFfwdListener =
457 new RepeatingImageButton.RepeatListener() {
458 public void onRepeat(View v, long howlong, int repcnt) {
459 scanForward(repcnt, howlong);
464 public void onStop() {
466 if (mService != null && mOneShot && getChangingConfigurations() == 0) {
469 } catch (RemoteException ex) {
472 mHandler.removeMessages(REFRESH);
473 unregisterReceiver(mStatusListener);
474 MusicUtils.unbindFromService(this);
480 public void onSaveInstanceState(Bundle outState) {
481 outState.putBoolean("oneshot", mOneShot);
482 super.onSaveInstanceState(outState);
486 public void onStart() {
490 if (false == MusicUtils.bindToService(this, osc)) {
491 // something went wrong
492 mHandler.sendEmptyMessage(QUIT);
495 IntentFilter f = new IntentFilter();
496 f.addAction(MediaPlaybackService.PLAYSTATE_CHANGED);
497 f.addAction(MediaPlaybackService.META_CHANGED);
498 f.addAction(MediaPlaybackService.PLAYBACK_COMPLETE);
499 registerReceiver(mStatusListener, new IntentFilter(f));
501 long next = refreshNow();
502 queueNextRefresh(next);
506 public void onNewIntent(Intent intent) {
508 mOneShot = intent.getBooleanExtra("oneshot", false);
512 public void onResume() {
515 setPauseButtonImage();
519 public void onDestroy()
521 mAlbumArtWorker.quit();
523 //System.out.println("***************** playback activity onDestroy\n");
527 public boolean onCreateOptionsMenu(Menu menu) {
528 super.onCreateOptionsMenu(menu);
529 // Don't show the menu items if we got launched by path/filedescriptor, or
530 // if we're in one shot mode. In most cases, these menu items are not
531 // useful in those modes, so for consistency we never show them in these
532 // modes, instead of tailoring them to the specific file being played.
533 if (MusicUtils.getCurrentAudioId() >= 0 && !mOneShot) {
534 menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library);
535 menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu()
536 SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0,
537 R.string.add_to_playlist).setIcon(android.R.drawable.ic_menu_add);
538 // these next two are in a separate group, so they can be shown/hidden as needed
539 // based on the keyguard state
540 menu.add(1, USE_AS_RINGTONE, 0, R.string.ringtone_menu_short)
541 .setIcon(R.drawable.ic_menu_set_as_ringtone);
542 menu.add(1, DELETE_ITEM, 0, R.string.delete_item)
543 .setIcon(R.drawable.ic_menu_delete);
550 public boolean onPrepareOptionsMenu(Menu menu) {
551 MenuItem item = menu.findItem(PARTY_SHUFFLE);
553 int shuffle = MusicUtils.getCurrentShuffleMode();
554 if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
555 item.setIcon(R.drawable.ic_menu_party_shuffle);
556 item.setTitle(R.string.party_shuffle_off);
558 item.setIcon(R.drawable.ic_menu_party_shuffle);
559 item.setTitle(R.string.party_shuffle);
563 item = menu.findItem(ADD_TO_PLAYLIST);
565 SubMenu sub = item.getSubMenu();
566 MusicUtils.makePlaylistMenu(this, sub);
569 KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
570 menu.setGroupVisible(1, !km.inKeyguardRestrictedInputMode());
576 public boolean onOptionsItemSelected(MenuItem item) {
579 switch (item.getItemId()) {
581 intent = new Intent();
582 intent.setClass(this, MusicBrowserActivity.class);
583 startActivity(intent);
586 case USE_AS_RINGTONE: {
587 // Set the system setting to make this the current ringtone
588 if (mService != null) {
589 MusicUtils.setRingtone(this, mService.getAudioId());
594 MusicUtils.togglePartyShuffle();
595 setShuffleButtonImage();
599 intent = new Intent();
600 intent.setClass(this, CreatePlaylist.class);
601 startActivityForResult(intent, NEW_PLAYLIST);
605 case PLAYLIST_SELECTED: {
606 long [] list = new long[1];
607 list[0] = MusicUtils.getCurrentAudioId();
608 long playlist = item.getIntent().getLongExtra("playlist", 0);
609 MusicUtils.addToPlaylist(this, list, playlist);
614 if (mService != null) {
615 long [] list = new long[1];
616 list[0] = MusicUtils.getCurrentAudioId();
617 Bundle b = new Bundle();
618 b.putString("description", getString(R.string.delete_song_desc,
619 mService.getTrackName()));
620 b.putLongArray("items", list);
621 intent = new Intent();
622 intent.setClass(this, DeleteItems.class);
624 startActivityForResult(intent, -1);
629 } catch (RemoteException ex) {
631 return super.onOptionsItemSelected(item);
635 protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
636 if (resultCode != RESULT_OK) {
639 switch (requestCode) {
641 Uri uri = intent.getData();
643 long [] list = new long[1];
644 list[0] = MusicUtils.getCurrentAudioId();
645 int playlist = Integer.parseInt(uri.getLastPathSegment());
646 MusicUtils.addToPlaylist(this, list, playlist);
651 private final int keyboard[][] = {
674 KeyEvent.KEYCODE_DEL,
684 KeyEvent.KEYCODE_COMMA,
685 KeyEvent.KEYCODE_PERIOD,
686 KeyEvent.KEYCODE_ENTER
694 private boolean seekMethod1(int keyCode)
696 if (mService == null) return false;
697 for(int x=0;x<10;x++) {
698 for(int y=0;y<3;y++) {
699 if(keyboard[y][x] == keyCode) {
702 if(x == lastX && y == lastY) dir = 0;
703 else if (y == 0 && lastY == 0 && x > lastX) dir = 1;
704 else if (y == 0 && lastY == 0 && x < lastX) dir = -1;
706 else if (y == 2 && lastY == 2 && x > lastX) dir = -1;
707 else if (y == 2 && lastY == 2 && x < lastX) dir = 1;
709 else if (y < lastY && x <= 4) dir = 1;
710 else if (y < lastY && x >= 5) dir = -1;
712 else if (y > lastY && x <= 4) dir = -1;
713 else if (y > lastY && x >= 5) dir = 1;
717 mService.seek(mService.position() + dir * 5);
718 } catch (RemoteException ex) {
730 private boolean seekMethod2(int keyCode)
732 if (mService == null) return false;
733 for(int i=0;i<10;i++) {
734 if(keyboard[0][i] == keyCode) {
735 int seekpercentage = 100*i/10;
737 mService.seek(mService.duration() * seekpercentage / 100);
738 } catch (RemoteException ex) {
748 public boolean onKeyUp(int keyCode, KeyEvent event) {
752 case KeyEvent.KEYCODE_DPAD_LEFT:
753 if (!useDpadMusicControl()) {
756 if (mService != null) {
757 if (!mSeeking && mStartSeekPos >= 0) {
758 mPauseButton.requestFocus();
759 if (mStartSeekPos < 1000) {
765 scanBackward(-1, event.getEventTime() - event.getDownTime());
766 mPauseButton.requestFocus();
773 case KeyEvent.KEYCODE_DPAD_RIGHT:
774 if (!useDpadMusicControl()) {
777 if (mService != null) {
778 if (!mSeeking && mStartSeekPos >= 0) {
779 mPauseButton.requestFocus();
782 scanForward(-1, event.getEventTime() - event.getDownTime());
783 mPauseButton.requestFocus();
791 } catch (RemoteException ex) {
793 return super.onKeyUp(keyCode, event);
796 private boolean useDpadMusicControl() {
797 if (mDeviceHasDpad && (mPrevButton.isFocused() ||
798 mNextButton.isFocused() ||
799 mPauseButton.isFocused())) {
806 public boolean onKeyDown(int keyCode, KeyEvent event)
809 int repcnt = event.getRepeatCount();
811 if((seekmethod==0)?seekMethod1(keyCode):seekMethod2(keyCode))
818 case KeyEvent.KEYCODE_Q: av.adjustParams(-0.05, 0.0, 0.0, 0.0, 0.0,-1.0); break;
819 case KeyEvent.KEYCODE_E: av.adjustParams( 0.05, 0.0, 0.0, 0.0, 0.0, 1.0); break;
821 case KeyEvent.KEYCODE_W: av.adjustParams( 0.0, 0.0,-1.0, 0.0, 0.0, 0.0); break;
822 case KeyEvent.KEYCODE_X: av.adjustParams( 0.0, 0.0, 1.0, 0.0, 0.0, 0.0); break;
823 case KeyEvent.KEYCODE_A: av.adjustParams( 0.0,-1.0, 0.0, 0.0, 0.0, 0.0); break;
824 case KeyEvent.KEYCODE_D: av.adjustParams( 0.0, 1.0, 0.0, 0.0, 0.0, 0.0); break;
826 case KeyEvent.KEYCODE_R: av.adjustParams( 0.0, 0.0, 0.0, 0.0, 0.0,-1.0); break;
827 case KeyEvent.KEYCODE_U: av.adjustParams( 0.0, 0.0, 0.0, 0.0, 0.0, 1.0); break;
829 case KeyEvent.KEYCODE_Y: av.adjustParams( 0.0, 0.0, 0.0, 0.0,-1.0, 0.0); break;
830 case KeyEvent.KEYCODE_N: av.adjustParams( 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); break;
831 case KeyEvent.KEYCODE_G: av.adjustParams( 0.0, 0.0, 0.0,-1.0, 0.0, 0.0); break;
832 case KeyEvent.KEYCODE_J: av.adjustParams( 0.0, 0.0, 0.0, 1.0, 0.0, 0.0); break;
836 case KeyEvent.KEYCODE_SLASH:
837 seekmethod = 1 - seekmethod;
840 case KeyEvent.KEYCODE_DPAD_LEFT:
841 if (!useDpadMusicControl()) {
844 if (!mPrevButton.hasFocus()) {
845 mPrevButton.requestFocus();
847 scanBackward(repcnt, event.getEventTime() - event.getDownTime());
849 case KeyEvent.KEYCODE_DPAD_RIGHT:
850 if (!useDpadMusicControl()) {
853 if (!mNextButton.hasFocus()) {
854 mNextButton.requestFocus();
856 scanForward(repcnt, event.getEventTime() - event.getDownTime());
859 case KeyEvent.KEYCODE_S:
863 case KeyEvent.KEYCODE_DPAD_CENTER:
864 case KeyEvent.KEYCODE_SPACE:
868 return super.onKeyDown(keyCode, event);
871 private void scanBackward(int repcnt, long delta) {
872 if(mService == null) return;
875 mStartSeekPos = mService.position();
876 mLastSeekEventTime = 0;
881 // seek at 10x speed for the first 5 seconds
884 // seek at 40x after that
885 delta = 50000 + (delta - 5000) * 40;
887 long newpos = mStartSeekPos - delta;
889 // move to previous track
891 long duration = mService.duration();
892 mStartSeekPos += duration;
895 if (((delta - mLastSeekEventTime) > 250) || repcnt < 0){
896 mService.seek(newpos);
897 mLastSeekEventTime = delta;
900 mPosOverride = newpos;
906 } catch (RemoteException ex) {
910 private void scanForward(int repcnt, long delta) {
911 if(mService == null) return;
914 mStartSeekPos = mService.position();
915 mLastSeekEventTime = 0;
920 // seek at 10x speed for the first 5 seconds
923 // seek at 40x after that
924 delta = 50000 + (delta - 5000) * 40;
926 long newpos = mStartSeekPos + delta;
927 long duration = mService.duration();
928 if (newpos >= duration) {
929 // move to next track
931 mStartSeekPos -= duration; // is OK to go negative
934 if (((delta - mLastSeekEventTime) > 250) || repcnt < 0){
935 mService.seek(newpos);
936 mLastSeekEventTime = delta;
939 mPosOverride = newpos;
945 } catch (RemoteException ex) {
949 private void doPauseResume() {
951 if(mService != null) {
952 if (mService.isPlaying()) {
958 setPauseButtonImage();
960 } catch (RemoteException ex) {
964 private void toggleShuffle() {
965 if (mService == null) {
969 int shuffle = mService.getShuffleMode();
970 if (shuffle == MediaPlaybackService.SHUFFLE_NONE) {
971 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
972 if (mService.getRepeatMode() == MediaPlaybackService.REPEAT_CURRENT) {
973 mService.setRepeatMode(MediaPlaybackService.REPEAT_ALL);
974 setRepeatButtonImage();
976 showToast(R.string.shuffle_on_notif);
977 } else if (shuffle == MediaPlaybackService.SHUFFLE_NORMAL ||
978 shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
979 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
980 showToast(R.string.shuffle_off_notif);
982 Log.e("MediaPlaybackActivity", "Invalid shuffle mode: " + shuffle);
984 setShuffleButtonImage();
985 } catch (RemoteException ex) {
989 private void cycleRepeat() {
990 if (mService == null) {
994 int mode = mService.getRepeatMode();
995 if (mode == MediaPlaybackService.REPEAT_NONE) {
996 mService.setRepeatMode(MediaPlaybackService.REPEAT_ALL);
997 showToast(R.string.repeat_all_notif);
998 } else if (mode == MediaPlaybackService.REPEAT_ALL) {
999 mService.setRepeatMode(MediaPlaybackService.REPEAT_CURRENT);
1000 if (mService.getShuffleMode() != MediaPlaybackService.SHUFFLE_NONE) {
1001 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
1002 setShuffleButtonImage();
1004 showToast(R.string.repeat_current_notif);
1006 mService.setRepeatMode(MediaPlaybackService.REPEAT_NONE);
1007 showToast(R.string.repeat_off_notif);
1009 setRepeatButtonImage();
1010 } catch (RemoteException ex) {
1015 private void showToast(int resid) {
1016 if (mToast == null) {
1017 mToast = Toast.makeText(this, "", Toast.LENGTH_SHORT);
1019 mToast.setText(resid);
1023 private void startPlayback() {
1025 if(mService == null)
1027 Intent intent = getIntent();
1028 String filename = "";
1029 Uri uri = intent.getData();
1030 if (uri != null && uri.toString().length() > 0) {
1031 // If this is a file:// URI, just use the path directly instead
1032 // of going through the open-from-filedescriptor codepath.
1033 String scheme = uri.getScheme();
1034 if ("file".equals(scheme)) {
1035 filename = uri.getPath();
1037 filename = uri.toString();
1040 if (! ContentResolver.SCHEME_CONTENT.equals(scheme) ||
1041 ! MediaStore.AUTHORITY.equals(uri.getAuthority())) {
1045 mService.openFile(filename, mOneShot);
1047 setIntent(new Intent());
1048 } catch (Exception ex) {
1049 Log.d("MediaPlaybackActivity", "couldn't start playback: " + ex);
1054 long next = refreshNow();
1055 queueNextRefresh(next);
1058 private ServiceConnection osc = new ServiceConnection() {
1059 public void onServiceConnected(ComponentName classname, IBinder obj) {
1060 mService = IMediaPlaybackService.Stub.asInterface(obj);
1063 // Assume something is playing when the service says it is,
1064 // but also if the audio ID is valid but the service is paused.
1065 if (mService.getAudioId() >= 0 || mService.isPlaying() ||
1066 mService.getPath() != null) {
1067 // something is playing now, we're done
1068 if (mOneShot || mService.getAudioId() < 0) {
1069 mRepeatButton.setVisibility(View.INVISIBLE);
1070 mShuffleButton.setVisibility(View.INVISIBLE);
1071 mQueueButton.setVisibility(View.INVISIBLE);
1073 mRepeatButton.setVisibility(View.VISIBLE);
1074 mShuffleButton.setVisibility(View.VISIBLE);
1075 mQueueButton.setVisibility(View.VISIBLE);
1076 setRepeatButtonImage();
1077 setShuffleButtonImage();
1079 setPauseButtonImage();
1082 } catch (RemoteException ex) {
1084 // Service is dead or not playing anything. If we got here as part
1085 // of a "play this file" Intent, exit. Otherwise go to the Music
1086 // app start screen.
1087 if (getIntent().getData() == null) {
1088 Intent intent = new Intent(Intent.ACTION_MAIN);
1089 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1090 intent.setClass(MediaPlaybackActivity.this, MusicBrowserActivity.class);
1091 startActivity(intent);
1095 public void onServiceDisconnected(ComponentName classname) {
1099 private void setRepeatButtonImage() {
1101 switch (mService.getRepeatMode()) {
1102 case MediaPlaybackService.REPEAT_ALL:
1103 mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_all_btn);
1105 case MediaPlaybackService.REPEAT_CURRENT:
1106 mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_once_btn);
1109 mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_off_btn);
1112 } catch (RemoteException ex) {
1116 private void setShuffleButtonImage() {
1118 switch (mService.getShuffleMode()) {
1119 case MediaPlaybackService.SHUFFLE_NONE:
1120 mShuffleButton.setImageResource(R.drawable.ic_mp_shuffle_off_btn);
1122 case MediaPlaybackService.SHUFFLE_AUTO:
1123 mShuffleButton.setImageResource(R.drawable.ic_mp_partyshuffle_on_btn);
1126 mShuffleButton.setImageResource(R.drawable.ic_mp_shuffle_on_btn);
1129 } catch (RemoteException ex) {
1133 private void setPauseButtonImage() {
1135 if (mService != null && mService.isPlaying()) {
1136 mPauseButton.setImageResource(android.R.drawable.ic_media_pause);
1138 mPauseButton.setImageResource(android.R.drawable.ic_media_play);
1140 } catch (RemoteException ex) {
1144 private ImageView mAlbum;
1145 private TextView mCurrentTime;
1146 private TextView mTotalTime;
1147 private TextView mArtistName;
1148 private TextView mAlbumName;
1149 private TextView mTrackName;
1150 private ProgressBar mProgress;
1151 private long mPosOverride = -1;
1152 private boolean mFromTouch = false;
1153 private long mDuration;
1154 private int seekmethod;
1155 private boolean paused;
1157 private static final int REFRESH = 1;
1158 private static final int QUIT = 2;
1159 private static final int GET_ALBUM_ART = 3;
1160 private static final int ALBUM_ART_DECODED = 4;
1162 private void queueNextRefresh(long delay) {
1164 Message msg = mHandler.obtainMessage(REFRESH);
1165 mHandler.removeMessages(REFRESH);
1166 mHandler.sendMessageDelayed(msg, delay);
1170 private long refreshNow() {
1171 if(mService == null)
1174 long pos = mPosOverride < 0 ? mService.position() : mPosOverride;
1175 long remaining = 1000 - (pos % 1000);
1176 if ((pos >= 0) && (mDuration > 0)) {
1177 mCurrentTime.setText(MusicUtils.makeTimeString(this, pos / 1000));
1179 if (mService.isPlaying()) {
1180 mCurrentTime.setVisibility(View.VISIBLE);
1182 // blink the counter
1183 int vis = mCurrentTime.getVisibility();
1184 mCurrentTime.setVisibility(vis == View.INVISIBLE ? View.VISIBLE : View.INVISIBLE);
1188 mProgress.setProgress((int) (1000 * pos / mDuration));
1190 mCurrentTime.setText("--:--");
1191 mProgress.setProgress(1000);
1193 // return the number of milliseconds until the next full second, so
1194 // the counter can be updated at just the right time
1196 } catch (RemoteException ex) {
1201 private final Handler mHandler = new Handler() {
1203 public void handleMessage(Message msg) {
1205 case ALBUM_ART_DECODED:
1206 mAlbum.setImageBitmap((Bitmap)msg.obj);
1207 mAlbum.getDrawable().setDither(true);
1211 long next = refreshNow();
1212 queueNextRefresh(next);
1216 // This can be moved back to onCreate once the bug that prevents
1217 // Dialogs from being started from onCreate/onResume is fixed.
1218 new AlertDialog.Builder(MediaPlaybackActivity.this)
1219 .setTitle(R.string.service_start_error_title)
1220 .setMessage(R.string.service_start_error_msg)
1221 .setPositiveButton(R.string.service_start_error_button,
1222 new DialogInterface.OnClickListener() {
1223 public void onClick(DialogInterface dialog, int whichButton) {
1227 .setCancelable(false)
1237 private BroadcastReceiver mStatusListener = new BroadcastReceiver() {
1239 public void onReceive(Context context, Intent intent) {
1240 String action = intent.getAction();
1241 if (action.equals(MediaPlaybackService.META_CHANGED)) {
1242 // redraw the artist/title info and
1243 // set new max for progress bar
1245 setPauseButtonImage();
1246 queueNextRefresh(1);
1247 } else if (action.equals(MediaPlaybackService.PLAYBACK_COMPLETE)) {
1251 setPauseButtonImage();
1253 } else if (action.equals(MediaPlaybackService.PLAYSTATE_CHANGED)) {
1254 setPauseButtonImage();
1259 private static class AlbumSongIdWrapper {
1260 public long albumid;
1262 AlbumSongIdWrapper(long aid, long sid) {
1268 private void updateTrackInfo() {
1269 if (mService == null) {
1273 String path = mService.getPath();
1279 long songid = mService.getAudioId();
1280 if (songid < 0 && path.toLowerCase().startsWith("http://")) {
1281 // Once we can get album art and meta data from MediaPlayer, we
1282 // can show that info again when streaming.
1283 ((View) mArtistName.getParent()).setVisibility(View.INVISIBLE);
1284 ((View) mAlbumName.getParent()).setVisibility(View.INVISIBLE);
1285 mAlbum.setVisibility(View.GONE);
1286 mTrackName.setText(path);
1287 mAlbumArtHandler.removeMessages(GET_ALBUM_ART);
1288 mAlbumArtHandler.obtainMessage(GET_ALBUM_ART, new AlbumSongIdWrapper(-1, -1)).sendToTarget();
1290 ((View) mArtistName.getParent()).setVisibility(View.VISIBLE);
1291 ((View) mAlbumName.getParent()).setVisibility(View.VISIBLE);
1292 String artistName = mService.getArtistName();
1293 if (MediaStore.UNKNOWN_STRING.equals(artistName)) {
1294 artistName = getString(R.string.unknown_artist_name);
1296 mArtistName.setText(artistName);
1297 String albumName = mService.getAlbumName();
1298 long albumid = mService.getAlbumId();
1299 if (MediaStore.UNKNOWN_STRING.equals(albumName)) {
1300 albumName = getString(R.string.unknown_album_name);
1303 mAlbumName.setText(albumName);
1304 mTrackName.setText(mService.getTrackName());
1305 mAlbumArtHandler.removeMessages(GET_ALBUM_ART);
1306 mAlbumArtHandler.obtainMessage(GET_ALBUM_ART, new AlbumSongIdWrapper(albumid, songid)).sendToTarget();
1307 mAlbum.setVisibility(View.VISIBLE);
1309 mDuration = mService.duration();
1310 mTotalTime.setText(MusicUtils.makeTimeString(this, mDuration / 1000));
1311 } catch (RemoteException ex) {
1316 public class AlbumArtHandler extends Handler {
1317 private long mAlbumId = -1;
1319 public AlbumArtHandler(Looper looper) {
1323 public void handleMessage(Message msg)
1325 long albumid = ((AlbumSongIdWrapper) msg.obj).albumid;
1326 long songid = ((AlbumSongIdWrapper) msg.obj).songid;
1327 if (msg.what == GET_ALBUM_ART && (mAlbumId != albumid || albumid < 0)) {
1328 // while decoding the new image, show the default album art
1329 Message numsg = mHandler.obtainMessage(ALBUM_ART_DECODED, null);
1330 mHandler.removeMessages(ALBUM_ART_DECODED);
1331 mHandler.sendMessageDelayed(numsg, 300);
1332 Bitmap bm = MusicUtils.getArtwork(MediaPlaybackActivity.this, songid, albumid);
1334 bm = MusicUtils.getArtwork(MediaPlaybackActivity.this, songid, -1);
1338 numsg = mHandler.obtainMessage(ALBUM_ART_DECODED, bm);
1339 mHandler.removeMessages(ALBUM_ART_DECODED);
1340 mHandler.sendMessage(numsg);
1347 private static class Worker implements Runnable {
1348 private final Object mLock = new Object();
1349 private Looper mLooper;
1352 * Creates a worker thread with the given name. The thread
1353 * then runs a {@link android.os.Looper}.
1354 * @param name A name for the new thread
1356 Worker(String name) {
1357 Thread t = new Thread(null, this, name);
1358 t.setPriority(Thread.MIN_PRIORITY);
1360 synchronized (mLock) {
1361 while (mLooper == null) {
1364 } catch (InterruptedException ex) {
1370 public Looper getLooper() {
1375 synchronized (mLock) {
1377 mLooper = Looper.myLooper();
1383 public void quit() {