OSDN Git Service

Merge commit 'goog/eclair-plus-aosp'
[android-x86/packages-apps-Music.git] / src / com / android / music / MediaPlaybackActivity.java
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.android.music;
18
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.content.res.Resources;
34 import android.database.Cursor;
35 import android.graphics.Bitmap;
36 import android.media.AudioManager;
37 import android.media.MediaFile;
38 import android.net.Uri;
39 import android.os.Bundle;
40 import android.os.RemoteException;
41 import android.os.Handler;
42 import android.os.IBinder;
43 import android.os.Looper;
44 import android.os.Message;
45 import android.os.SystemClock;
46 import android.provider.MediaStore;
47 import android.text.Layout;
48 import android.text.TextUtils.TruncateAt;
49 import android.util.Log;
50 import android.view.KeyEvent;
51 import android.view.Menu;
52 import android.view.MenuItem;
53 import android.view.MotionEvent;
54 import android.view.SubMenu;
55 import android.view.View;
56 import android.view.ViewConfiguration;
57 import android.view.Window;
58 import android.view.WindowManager;
59 import android.widget.ImageButton;
60 import android.widget.ImageView;
61 import android.widget.ProgressBar;
62 import android.widget.SeekBar;
63 import android.widget.TextView;
64 import android.widget.Toast;
65 import android.widget.SeekBar.OnSeekBarChangeListener;
66
67
68 public class MediaPlaybackActivity extends Activity implements MusicUtils.Defs,
69     View.OnTouchListener, View.OnLongClickListener
70 {
71     private static final int USE_AS_RINGTONE = CHILD_MENU_BASE;
72     
73     private boolean mOneShot = false;
74     private boolean mSeeking = false;
75     private boolean mDeviceHasDpad;
76     private long mStartSeekPos = 0;
77     private long mLastSeekEventTime;
78     private IMediaPlaybackService mService = null;
79     private RepeatingImageButton mPrevButton;
80     private ImageButton mPauseButton;
81     private RepeatingImageButton mNextButton;
82     private ImageButton mRepeatButton;
83     private ImageButton mShuffleButton;
84     private ImageButton mQueueButton;
85     private Worker mAlbumArtWorker;
86     private AlbumArtHandler mAlbumArtHandler;
87     private Toast mToast;
88     private int mTouchSlop;
89
90     public MediaPlaybackActivity()
91     {
92     }
93
94     /** Called when the activity is first created. */
95     @Override
96     public void onCreate(Bundle icicle)
97     {
98         super.onCreate(icicle);
99         setVolumeControlStream(AudioManager.STREAM_MUSIC);
100
101         mAlbumArtWorker = new Worker("album art worker");
102         mAlbumArtHandler = new AlbumArtHandler(mAlbumArtWorker.getLooper());
103
104         requestWindowFeature(Window.FEATURE_NO_TITLE);
105         setContentView(R.layout.audio_player);
106
107         mCurrentTime = (TextView) findViewById(R.id.currenttime);
108         mTotalTime = (TextView) findViewById(R.id.totaltime);
109         mProgress = (ProgressBar) findViewById(android.R.id.progress);
110         mAlbum = (ImageView) findViewById(R.id.album);
111         mArtistName = (TextView) findViewById(R.id.artistname);
112         mAlbumName = (TextView) findViewById(R.id.albumname);
113         mTrackName = (TextView) findViewById(R.id.trackname);
114
115         View v = (View)mArtistName.getParent(); 
116         v.setOnTouchListener(this);
117         v.setOnLongClickListener(this);
118
119         v = (View)mAlbumName.getParent();
120         v.setOnTouchListener(this);
121         v.setOnLongClickListener(this);
122
123         v = (View)mTrackName.getParent();
124         v.setOnTouchListener(this);
125         v.setOnLongClickListener(this);
126         
127         mPrevButton = (RepeatingImageButton) findViewById(R.id.prev);
128         mPrevButton.setOnClickListener(mPrevListener);
129         mPrevButton.setRepeatListener(mRewListener, 260);
130         mPauseButton = (ImageButton) findViewById(R.id.pause);
131         mPauseButton.requestFocus();
132         mPauseButton.setOnClickListener(mPauseListener);
133         mNextButton = (RepeatingImageButton) findViewById(R.id.next);
134         mNextButton.setOnClickListener(mNextListener);
135         mNextButton.setRepeatListener(mFfwdListener, 260);
136         seekmethod = 1;
137
138         mDeviceHasDpad = (getResources().getConfiguration().navigation ==
139             Configuration.NAVIGATION_DPAD);
140         
141         mQueueButton = (ImageButton) findViewById(R.id.curplaylist);
142         mQueueButton.setOnClickListener(mQueueListener);
143         mShuffleButton = ((ImageButton) findViewById(R.id.shuffle));
144         mShuffleButton.setOnClickListener(mShuffleListener);
145         mRepeatButton = ((ImageButton) findViewById(R.id.repeat));
146         mRepeatButton.setOnClickListener(mRepeatListener);
147         
148         if (mProgress instanceof SeekBar) {
149             SeekBar seeker = (SeekBar) mProgress;
150             seeker.setOnSeekBarChangeListener(mSeekListener);
151         }
152         mProgress.setMax(1000);
153         
154         if (icicle != null) {
155             mOneShot = icicle.getBoolean("oneshot");
156         } else {
157             mOneShot = getIntent().getBooleanExtra("oneshot", false);
158         }
159
160         mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
161     }
162     
163     int mInitialX = -1;
164     int mLastX = -1;
165     int mTextWidth = 0;
166     int mViewWidth = 0;
167     boolean mDraggingLabel = false;
168     
169     TextView textViewForContainer(View v) {
170         View vv = v.findViewById(R.id.artistname);
171         if (vv != null) return (TextView) vv;
172         vv = v.findViewById(R.id.albumname);
173         if (vv != null) return (TextView) vv;
174         vv = v.findViewById(R.id.trackname);
175         if (vv != null) return (TextView) vv;
176         return null;
177     }
178     
179     public boolean onTouch(View v, MotionEvent event) {
180         int action = event.getAction();
181         TextView tv = textViewForContainer(v);
182         if (tv == null) {
183             return false;
184         }
185         if (action == MotionEvent.ACTION_DOWN) {
186             v.setBackgroundColor(0xff606060);
187             mInitialX = mLastX = (int) event.getX();
188             mDraggingLabel = false;
189         } else if (action == MotionEvent.ACTION_UP ||
190                 action == MotionEvent.ACTION_CANCEL) {
191             v.setBackgroundColor(0);
192             if (mDraggingLabel) {
193                 Message msg = mLabelScroller.obtainMessage(0, tv);
194                 mLabelScroller.sendMessageDelayed(msg, 1000);
195             }
196         } else if (action == MotionEvent.ACTION_MOVE) {
197             if (mDraggingLabel) {
198                 int scrollx = tv.getScrollX();
199                 int x = (int) event.getX();
200                 int delta = mLastX - x;
201                 if (delta != 0) {
202                     mLastX = x;
203                     scrollx += delta;
204                     if (scrollx > mTextWidth) {
205                         // scrolled the text completely off the view to the left
206                         scrollx -= mTextWidth;
207                         scrollx -= mViewWidth;
208                     }
209                     if (scrollx < -mViewWidth) {
210                         // scrolled the text completely off the view to the right
211                         scrollx += mViewWidth;
212                         scrollx += mTextWidth;
213                     }
214                     tv.scrollTo(scrollx, 0);
215                 }
216                 return true;
217             }
218             int delta = mInitialX - (int) event.getX();
219             if (Math.abs(delta) > mTouchSlop) {
220                 // start moving
221                 mLabelScroller.removeMessages(0, tv);
222                 
223                 // Only turn ellipsizing off when it's not already off, because it
224                 // causes the scroll position to be reset to 0.
225                 if (tv.getEllipsize() != null) {
226                     tv.setEllipsize(null);
227                 }
228                 Layout ll = tv.getLayout();
229                 // layout might be null if the text just changed, or ellipsizing
230                 // was just turned off
231                 if (ll == null) {
232                     return false;
233                 }
234                 // get the non-ellipsized line width, to determine whether scrolling
235                 // should even be allowed
236                 mTextWidth = (int) tv.getLayout().getLineWidth(0);
237                 mViewWidth = tv.getWidth();
238                 if (mViewWidth > mTextWidth) {
239                     tv.setEllipsize(TruncateAt.END);
240                     v.cancelLongPress();
241                     return false;
242                 }
243                 mDraggingLabel = true;
244                 tv.setHorizontalFadingEdgeEnabled(true);
245                 v.cancelLongPress();
246                 return true;
247             }
248         }
249         return false; 
250     }
251
252     Handler mLabelScroller = new Handler() {
253         @Override
254         public void handleMessage(Message msg) {
255             TextView tv = (TextView) msg.obj;
256             int x = tv.getScrollX();
257             x = x * 3 / 4;
258             tv.scrollTo(x, 0);
259             if (x == 0) {
260                 tv.setEllipsize(TruncateAt.END);
261             } else {
262                 Message newmsg = obtainMessage(0, tv);
263                 mLabelScroller.sendMessageDelayed(newmsg, 15);
264             }
265         }
266     };
267     
268     public boolean onLongClick(View view) {
269
270         CharSequence title = null;
271         String mime = null;
272         String query = null;
273         String artist;
274         String album;
275         String song;
276         long audioid;
277         
278         try {
279             artist = mService.getArtistName();
280             album = mService.getAlbumName();
281             song = mService.getTrackName();
282             audioid = mService.getAudioId();
283         } catch (RemoteException ex) {
284             return true;
285         } catch (NullPointerException ex) {
286             // we might not actually have the service yet
287             return true;
288         }
289
290         if (MediaFile.UNKNOWN_STRING.equals(album) &&
291                 MediaFile.UNKNOWN_STRING.equals(artist) &&
292                 song != null &&
293                 song.startsWith("recording")) {
294             // not music
295             return false;
296         }
297
298         if (audioid < 0) {
299             return false;
300         }
301
302         Cursor c = MusicUtils.query(this,
303                 ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, audioid),
304                 new String[] {MediaStore.Audio.Media.IS_MUSIC}, null, null, null);
305         boolean ismusic = true;
306         if (c != null) {
307             if (c.moveToFirst()) {
308                 ismusic = c.getInt(0) != 0;
309             }
310             c.close();
311         }
312         if (!ismusic) {
313             return false;
314         }
315
316         boolean knownartist =
317             (artist != null) && !MediaFile.UNKNOWN_STRING.equals(artist);
318
319         boolean knownalbum =
320             (album != null) && !MediaFile.UNKNOWN_STRING.equals(album);
321         
322         if (knownartist && view.equals(mArtistName.getParent())) {
323             title = artist;
324             query = artist;
325             mime = MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE;
326         } else if (knownalbum && view.equals(mAlbumName.getParent())) {
327             title = album;
328             if (knownartist) {
329                 query = artist + " " + album;
330             } else {
331                 query = album;
332             }
333             mime = MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE;
334         } else if (view.equals(mTrackName.getParent()) || !knownartist || !knownalbum) {
335             if ((song == null) || MediaFile.UNKNOWN_STRING.equals(song)) {
336                 // A popup of the form "Search for null/'' using ..." is pretty
337                 // unhelpful, plus, we won't find any way to buy it anyway.
338                 return true;
339             }
340
341             title = song;
342             if (knownartist) {
343                 query = artist + " " + song;
344             } else {
345                 query = song;
346             }
347             mime = "audio/*"; // the specific type doesn't matter, so don't bother retrieving it
348         } else {
349             throw new RuntimeException("shouldn't be here");
350         }
351         title = getString(R.string.mediasearch, title);
352
353         Intent i = new Intent();
354         i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
355         i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
356         i.putExtra(SearchManager.QUERY, query);
357         if(knownartist) {
358             i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, artist);
359         }
360         if(knownalbum) {
361             i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, album);
362         }
363         i.putExtra(MediaStore.EXTRA_MEDIA_TITLE, song);
364         i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, mime);
365
366         startActivity(Intent.createChooser(i, title));
367         return true;
368     }
369
370     private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
371         public void onStartTrackingTouch(SeekBar bar) {
372             mLastSeekEventTime = 0;
373             mFromTouch = true;
374         }
375         public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
376             if (!fromuser || (mService == null)) return;
377             long now = SystemClock.elapsedRealtime();
378             if ((now - mLastSeekEventTime) > 250) {
379                 mLastSeekEventTime = now;
380                 mPosOverride = mDuration * progress / 1000;
381                 try {
382                     mService.seek(mPosOverride);
383                 } catch (RemoteException ex) {
384                 }
385
386                 // trackball event, allow progress updates
387                 if (!mFromTouch) {
388                     refreshNow();
389                     mPosOverride = -1;
390                 }
391             }
392         }
393         public void onStopTrackingTouch(SeekBar bar) {
394             mPosOverride = -1;
395             mFromTouch = false;
396         }
397     };
398     
399     private View.OnClickListener mQueueListener = new View.OnClickListener() {
400         public void onClick(View v) {
401             startActivity(
402                     new Intent(Intent.ACTION_EDIT)
403                     .setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track")
404                     .putExtra("playlist", "nowplaying")
405             );
406         }
407     };
408     
409     private View.OnClickListener mShuffleListener = new View.OnClickListener() {
410         public void onClick(View v) {
411             toggleShuffle();
412         }
413     };
414
415     private View.OnClickListener mRepeatListener = new View.OnClickListener() {
416         public void onClick(View v) {
417             cycleRepeat();
418         }
419     };
420
421     private View.OnClickListener mPauseListener = new View.OnClickListener() {
422         public void onClick(View v) {
423             doPauseResume();
424         }
425     };
426
427     private View.OnClickListener mPrevListener = new View.OnClickListener() {
428         public void onClick(View v) {
429             if (mService == null) return;
430             try {
431                 if (mService.position() < 2000) {
432                     mService.prev();
433                 } else {
434                     mService.seek(0);
435                     mService.play();
436                 }
437             } catch (RemoteException ex) {
438             }
439         }
440     };
441
442     private View.OnClickListener mNextListener = new View.OnClickListener() {
443         public void onClick(View v) {
444             if (mService == null) return;
445             try {
446                 mService.next();
447             } catch (RemoteException ex) {
448             }
449         }
450     };
451
452     private RepeatingImageButton.RepeatListener mRewListener =
453         new RepeatingImageButton.RepeatListener() {
454         public void onRepeat(View v, long howlong, int repcnt) {
455             scanBackward(repcnt, howlong);
456         }
457     };
458     
459     private RepeatingImageButton.RepeatListener mFfwdListener =
460         new RepeatingImageButton.RepeatListener() {
461         public void onRepeat(View v, long howlong, int repcnt) {
462             scanForward(repcnt, howlong);
463         }
464     };
465    
466     @Override
467     public void onStop() {
468         paused = true;
469         if (mService != null && mOneShot && getChangingConfigurations() == 0) {
470             try {
471                 mService.stop();
472             } catch (RemoteException ex) {
473             }
474         }
475         mHandler.removeMessages(REFRESH);
476         unregisterReceiver(mStatusListener);
477         MusicUtils.unbindFromService(this);
478         mService = null;
479         super.onStop();
480     }
481
482     @Override
483     public void onSaveInstanceState(Bundle outState) {
484         outState.putBoolean("oneshot", mOneShot);
485         super.onSaveInstanceState(outState);
486     }
487     
488     @Override
489     public void onStart() {
490         super.onStart();
491         paused = false;
492
493         if (false == MusicUtils.bindToService(this, osc)) {
494             // something went wrong
495             mHandler.sendEmptyMessage(QUIT);
496         }
497         
498         IntentFilter f = new IntentFilter();
499         f.addAction(MediaPlaybackService.PLAYSTATE_CHANGED);
500         f.addAction(MediaPlaybackService.META_CHANGED);
501         f.addAction(MediaPlaybackService.PLAYBACK_COMPLETE);
502         registerReceiver(mStatusListener, new IntentFilter(f));
503         updateTrackInfo();
504         long next = refreshNow();
505         queueNextRefresh(next);
506     }
507     
508     @Override
509     public void onNewIntent(Intent intent) {
510         setIntent(intent);
511         mOneShot = intent.getBooleanExtra("oneshot", false);
512     }
513     
514     @Override
515     public void onResume() {
516         super.onResume();
517         updateTrackInfo();
518         setPauseButtonImage();
519     }
520     
521     @Override
522     public void onDestroy()
523     {
524         mAlbumArtWorker.quit();
525         super.onDestroy();
526         //System.out.println("***************** playback activity onDestroy\n");
527     }
528
529     @Override
530     public boolean onCreateOptionsMenu(Menu menu) {
531         super.onCreateOptionsMenu(menu);
532         // Don't show the menu items if we got launched by path/filedescriptor, or
533         // if we're in one shot mode. In most cases, these menu items are not
534         // useful in those modes, so for consistency we never show them in these
535         // modes, instead of tailoring them to the specific file being played.
536         if (MusicUtils.getCurrentAudioId() >= 0 && !mOneShot) {
537             menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library);
538             menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu()
539             SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0,
540                     R.string.add_to_playlist).setIcon(android.R.drawable.ic_menu_add);
541             // these next two are in a separate group, so they can be shown/hidden as needed
542             // based on the keyguard state
543             menu.add(1, USE_AS_RINGTONE, 0, R.string.ringtone_menu_short)
544                     .setIcon(R.drawable.ic_menu_set_as_ringtone);
545             menu.add(1, DELETE_ITEM, 0, R.string.delete_item)
546                     .setIcon(R.drawable.ic_menu_delete);
547             return true;
548         }
549         return false;
550     }
551
552     @Override
553     public boolean onPrepareOptionsMenu(Menu menu) {
554         MenuItem item = menu.findItem(PARTY_SHUFFLE);
555         if (item != null) {
556             int shuffle = MusicUtils.getCurrentShuffleMode();
557             if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
558                 item.setIcon(R.drawable.ic_menu_party_shuffle);
559                 item.setTitle(R.string.party_shuffle_off);
560             } else {
561                 item.setIcon(R.drawable.ic_menu_party_shuffle);
562                 item.setTitle(R.string.party_shuffle);
563             }
564         }
565
566         item = menu.findItem(ADD_TO_PLAYLIST);
567         if (item != null) {
568             SubMenu sub = item.getSubMenu();
569             MusicUtils.makePlaylistMenu(this, sub);
570         }
571
572         KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
573         menu.setGroupVisible(1, !km.inKeyguardRestrictedInputMode());
574
575         return true;
576     }
577
578     @Override
579     public boolean onOptionsItemSelected(MenuItem item) {
580         Intent intent;
581         try {
582             switch (item.getItemId()) {
583                 case GOTO_START:
584                     intent = new Intent();
585                     intent.setClass(this, MusicBrowserActivity.class);
586                     startActivity(intent);
587                     finish();
588                     break;
589                 case USE_AS_RINGTONE: {
590                     // Set the system setting to make this the current ringtone
591                     if (mService != null) {
592                         MusicUtils.setRingtone(this, mService.getAudioId());
593                     }
594                     return true;
595                 }
596                 case PARTY_SHUFFLE:
597                     MusicUtils.togglePartyShuffle();
598                     setShuffleButtonImage();
599                     break;
600                     
601                 case NEW_PLAYLIST: {
602                     intent = new Intent();
603                     intent.setClass(this, CreatePlaylist.class);
604                     startActivityForResult(intent, NEW_PLAYLIST);
605                     return true;
606                 }
607
608                 case PLAYLIST_SELECTED: {
609                     long [] list = new long[1];
610                     list[0] = MusicUtils.getCurrentAudioId();
611                     long playlist = item.getIntent().getLongExtra("playlist", 0);
612                     MusicUtils.addToPlaylist(this, list, playlist);
613                     return true;
614                 }
615                 
616                 case DELETE_ITEM: {
617                     if (mService != null) {
618                         long [] list = new long[1];
619                         list[0] = MusicUtils.getCurrentAudioId();
620                         Bundle b = new Bundle();
621                         b.putString("description", getString(R.string.delete_song_desc,
622                                 mService.getTrackName()));
623                         b.putLongArray("items", list);
624                         intent = new Intent();
625                         intent.setClass(this, DeleteItems.class);
626                         intent.putExtras(b);
627                         startActivityForResult(intent, -1);
628                     }
629                     return true;
630                 }
631             }
632         } catch (RemoteException ex) {
633         }
634         return super.onOptionsItemSelected(item);
635     }
636     
637     @Override
638     protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
639         if (resultCode != RESULT_OK) {
640             return;
641         }
642         switch (requestCode) {
643             case NEW_PLAYLIST:
644                 Uri uri = intent.getData();
645                 if (uri != null) {
646                     long [] list = new long[1];
647                     list[0] = MusicUtils.getCurrentAudioId();
648                     int playlist = Integer.parseInt(uri.getLastPathSegment());
649                     MusicUtils.addToPlaylist(this, list, playlist);
650                 }
651                 break;
652         }
653     }
654     private final int keyboard[][] = {
655         {
656             KeyEvent.KEYCODE_Q,
657             KeyEvent.KEYCODE_W,
658             KeyEvent.KEYCODE_E,
659             KeyEvent.KEYCODE_R,
660             KeyEvent.KEYCODE_T,
661             KeyEvent.KEYCODE_Y,
662             KeyEvent.KEYCODE_U,
663             KeyEvent.KEYCODE_I,
664             KeyEvent.KEYCODE_O,
665             KeyEvent.KEYCODE_P,
666         },
667         {
668             KeyEvent.KEYCODE_A,
669             KeyEvent.KEYCODE_S,
670             KeyEvent.KEYCODE_D,
671             KeyEvent.KEYCODE_F,
672             KeyEvent.KEYCODE_G,
673             KeyEvent.KEYCODE_H,
674             KeyEvent.KEYCODE_J,
675             KeyEvent.KEYCODE_K,
676             KeyEvent.KEYCODE_L,
677             KeyEvent.KEYCODE_DEL,
678         },
679         {
680             KeyEvent.KEYCODE_Z,
681             KeyEvent.KEYCODE_X,
682             KeyEvent.KEYCODE_C,
683             KeyEvent.KEYCODE_V,
684             KeyEvent.KEYCODE_B,
685             KeyEvent.KEYCODE_N,
686             KeyEvent.KEYCODE_M,
687             KeyEvent.KEYCODE_COMMA,
688             KeyEvent.KEYCODE_PERIOD,
689             KeyEvent.KEYCODE_ENTER
690         }
691
692     };
693
694     private int lastX;
695     private int lastY;
696
697     private boolean seekMethod1(int keyCode)
698     {
699         if (mService == null) return false;
700         for(int x=0;x<10;x++) {
701             for(int y=0;y<3;y++) {
702                 if(keyboard[y][x] == keyCode) {
703                     int dir = 0;
704                     // top row
705                     if(x == lastX && y == lastY) dir = 0;
706                     else if (y == 0 && lastY == 0 && x > lastX) dir = 1;
707                     else if (y == 0 && lastY == 0 && x < lastX) dir = -1;
708                     // bottom row
709                     else if (y == 2 && lastY == 2 && x > lastX) dir = -1;
710                     else if (y == 2 && lastY == 2 && x < lastX) dir = 1;
711                     // moving up
712                     else if (y < lastY && x <= 4) dir = 1; 
713                     else if (y < lastY && x >= 5) dir = -1; 
714                     // moving down
715                     else if (y > lastY && x <= 4) dir = -1; 
716                     else if (y > lastY && x >= 5) dir = 1; 
717                     lastX = x;
718                     lastY = y;
719                     try {
720                         mService.seek(mService.position() + dir * 5);
721                     } catch (RemoteException ex) {
722                     }
723                     refreshNow();
724                     return true;
725                 }
726             }
727         }
728         lastX = -1;
729         lastY = -1;
730         return false;
731     }
732
733     private boolean seekMethod2(int keyCode)
734     {
735         if (mService == null) return false;
736         for(int i=0;i<10;i++) {
737             if(keyboard[0][i] == keyCode) {
738                 int seekpercentage = 100*i/10;
739                 try {
740                     mService.seek(mService.duration() * seekpercentage / 100);
741                 } catch (RemoteException ex) {
742                 }
743                 refreshNow();
744                 return true;
745             }
746         }
747         return false;
748     }
749
750     @Override
751     public boolean onKeyUp(int keyCode, KeyEvent event) {
752         try {
753             switch(keyCode)
754             {
755                 case KeyEvent.KEYCODE_DPAD_LEFT:
756                     if (!useDpadMusicControl()) {
757                         break;
758                     }
759                     if (mService != null) {
760                         if (!mSeeking && mStartSeekPos >= 0) {
761                             mPauseButton.requestFocus();
762                             if (mStartSeekPos < 1000) {
763                                 mService.prev();
764                             } else {
765                                 mService.seek(0);
766                             }
767                         } else {
768                             scanBackward(-1, event.getEventTime() - event.getDownTime());
769                             mPauseButton.requestFocus();
770                             mStartSeekPos = -1;
771                         }
772                     }
773                     mSeeking = false;
774                     mPosOverride = -1;
775                     return true;
776                 case KeyEvent.KEYCODE_DPAD_RIGHT:
777                     if (!useDpadMusicControl()) {
778                         break;
779                     }
780                     if (mService != null) {
781                         if (!mSeeking && mStartSeekPos >= 0) {
782                             mPauseButton.requestFocus();
783                             mService.next();
784                         } else {
785                             scanForward(-1, event.getEventTime() - event.getDownTime());
786                             mPauseButton.requestFocus();
787                             mStartSeekPos = -1;
788                         }
789                     }
790                     mSeeking = false;
791                     mPosOverride = -1;
792                     return true;
793             }
794         } catch (RemoteException ex) {
795         }
796         return super.onKeyUp(keyCode, event);
797     }
798
799     private boolean useDpadMusicControl() {
800         if (mDeviceHasDpad && (mPrevButton.isFocused() ||
801                 mNextButton.isFocused() ||
802                 mPauseButton.isFocused())) {
803             return true;
804         }
805         return false;
806     }
807
808     @Override
809     public boolean onKeyDown(int keyCode, KeyEvent event)
810     {
811         int direction = -1;
812         int repcnt = event.getRepeatCount();
813
814         if((seekmethod==0)?seekMethod1(keyCode):seekMethod2(keyCode))
815             return true;
816
817         switch(keyCode)
818         {
819 /*
820             // image scale
821             case KeyEvent.KEYCODE_Q: av.adjustParams(-0.05, 0.0, 0.0, 0.0, 0.0,-1.0); break;
822             case KeyEvent.KEYCODE_E: av.adjustParams( 0.05, 0.0, 0.0, 0.0, 0.0, 1.0); break;
823             // image translate
824             case KeyEvent.KEYCODE_W: av.adjustParams(    0.0, 0.0,-1.0, 0.0, 0.0, 0.0); break;
825             case KeyEvent.KEYCODE_X: av.adjustParams(    0.0, 0.0, 1.0, 0.0, 0.0, 0.0); break;
826             case KeyEvent.KEYCODE_A: av.adjustParams(    0.0,-1.0, 0.0, 0.0, 0.0, 0.0); break;
827             case KeyEvent.KEYCODE_D: av.adjustParams(    0.0, 1.0, 0.0, 0.0, 0.0, 0.0); break;
828             // camera rotation
829             case KeyEvent.KEYCODE_R: av.adjustParams(    0.0, 0.0, 0.0, 0.0, 0.0,-1.0); break;
830             case KeyEvent.KEYCODE_U: av.adjustParams(    0.0, 0.0, 0.0, 0.0, 0.0, 1.0); break;
831             // camera translate
832             case KeyEvent.KEYCODE_Y: av.adjustParams(    0.0, 0.0, 0.0, 0.0,-1.0, 0.0); break;
833             case KeyEvent.KEYCODE_N: av.adjustParams(    0.0, 0.0, 0.0, 0.0, 1.0, 0.0); break;
834             case KeyEvent.KEYCODE_G: av.adjustParams(    0.0, 0.0, 0.0,-1.0, 0.0, 0.0); break;
835             case KeyEvent.KEYCODE_J: av.adjustParams(    0.0, 0.0, 0.0, 1.0, 0.0, 0.0); break;
836
837 */
838
839             case KeyEvent.KEYCODE_SLASH:
840                 seekmethod = 1 - seekmethod;
841                 return true;
842
843             case KeyEvent.KEYCODE_DPAD_LEFT:
844                 if (!useDpadMusicControl()) {
845                     break;
846                 }
847                 if (!mPrevButton.hasFocus()) {
848                     mPrevButton.requestFocus();
849                 }
850                 scanBackward(repcnt, event.getEventTime() - event.getDownTime());
851                 return true;
852             case KeyEvent.KEYCODE_DPAD_RIGHT:
853                 if (!useDpadMusicControl()) {
854                     break;
855                 }
856                 if (!mNextButton.hasFocus()) {
857                     mNextButton.requestFocus();
858                 }
859                 scanForward(repcnt, event.getEventTime() - event.getDownTime());
860                 return true;
861
862             case KeyEvent.KEYCODE_S:
863                 toggleShuffle();
864                 return true;
865
866             case KeyEvent.KEYCODE_DPAD_CENTER:
867             case KeyEvent.KEYCODE_SPACE:
868                 doPauseResume();
869                 return true;
870         }
871         return super.onKeyDown(keyCode, event);
872     }
873     
874     private void scanBackward(int repcnt, long delta) {
875         if(mService == null) return;
876         try {
877             if(repcnt == 0) {
878                 mStartSeekPos = mService.position();
879                 mLastSeekEventTime = 0;
880                 mSeeking = false;
881             } else {
882                 mSeeking = true;
883                 if (delta < 5000) {
884                     // seek at 10x speed for the first 5 seconds
885                     delta = delta * 10; 
886                 } else {
887                     // seek at 40x after that
888                     delta = 50000 + (delta - 5000) * 40;
889                 }
890                 long newpos = mStartSeekPos - delta;
891                 if (newpos < 0) {
892                     // move to previous track
893                     mService.prev();
894                     long duration = mService.duration();
895                     mStartSeekPos += duration;
896                     newpos += duration;
897                 }
898                 if (((delta - mLastSeekEventTime) > 250) || repcnt < 0){
899                     mService.seek(newpos);
900                     mLastSeekEventTime = delta;
901                 }
902                 if (repcnt >= 0) {
903                     mPosOverride = newpos;
904                 } else {
905                     mPosOverride = -1;
906                 }
907                 refreshNow();
908             }
909         } catch (RemoteException ex) {
910         }
911     }
912
913     private void scanForward(int repcnt, long delta) {
914         if(mService == null) return;
915         try {
916             if(repcnt == 0) {
917                 mStartSeekPos = mService.position();
918                 mLastSeekEventTime = 0;
919                 mSeeking = false;
920             } else {
921                 mSeeking = true;
922                 if (delta < 5000) {
923                     // seek at 10x speed for the first 5 seconds
924                     delta = delta * 10; 
925                 } else {
926                     // seek at 40x after that
927                     delta = 50000 + (delta - 5000) * 40;
928                 }
929                 long newpos = mStartSeekPos + delta;
930                 long duration = mService.duration();
931                 if (newpos >= duration) {
932                     // move to next track
933                     mService.next();
934                     mStartSeekPos -= duration; // is OK to go negative
935                     newpos -= duration;
936                 }
937                 if (((delta - mLastSeekEventTime) > 250) || repcnt < 0){
938                     mService.seek(newpos);
939                     mLastSeekEventTime = delta;
940                 }
941                 if (repcnt >= 0) {
942                     mPosOverride = newpos;
943                 } else {
944                     mPosOverride = -1;
945                 }
946                 refreshNow();
947             }
948         } catch (RemoteException ex) {
949         }
950     }
951     
952     private void doPauseResume() {
953         try {
954             if(mService != null) {
955                 if (mService.isPlaying()) {
956                     mService.pause();
957                 } else {
958                     mService.play();
959                 }
960                 refreshNow();
961                 setPauseButtonImage();
962             }
963         } catch (RemoteException ex) {
964         }
965     }
966     
967     private void toggleShuffle() {
968         if (mService == null) {
969             return;
970         }
971         try {
972             int shuffle = mService.getShuffleMode();
973             if (shuffle == MediaPlaybackService.SHUFFLE_NONE) {
974                 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
975                 if (mService.getRepeatMode() == MediaPlaybackService.REPEAT_CURRENT) {
976                     mService.setRepeatMode(MediaPlaybackService.REPEAT_ALL);
977                     setRepeatButtonImage();
978                 }
979                 showToast(R.string.shuffle_on_notif);
980             } else if (shuffle == MediaPlaybackService.SHUFFLE_NORMAL ||
981                     shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
982                 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
983                 showToast(R.string.shuffle_off_notif);
984             } else {
985                 Log.e("MediaPlaybackActivity", "Invalid shuffle mode: " + shuffle);
986             }
987             setShuffleButtonImage();
988         } catch (RemoteException ex) {
989         }
990     }
991     
992     private void cycleRepeat() {
993         if (mService == null) {
994             return;
995         }
996         try {
997             int mode = mService.getRepeatMode();
998             if (mode == MediaPlaybackService.REPEAT_NONE) {
999                 mService.setRepeatMode(MediaPlaybackService.REPEAT_ALL);
1000                 showToast(R.string.repeat_all_notif);
1001             } else if (mode == MediaPlaybackService.REPEAT_ALL) {
1002                 mService.setRepeatMode(MediaPlaybackService.REPEAT_CURRENT);
1003                 if (mService.getShuffleMode() != MediaPlaybackService.SHUFFLE_NONE) {
1004                     mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
1005                     setShuffleButtonImage();
1006                 }
1007                 showToast(R.string.repeat_current_notif);
1008             } else {
1009                 mService.setRepeatMode(MediaPlaybackService.REPEAT_NONE);
1010                 showToast(R.string.repeat_off_notif);
1011             }
1012             setRepeatButtonImage();
1013         } catch (RemoteException ex) {
1014         }
1015         
1016     }
1017     
1018     private void showToast(int resid) {
1019         if (mToast == null) {
1020             mToast = Toast.makeText(this, "", Toast.LENGTH_SHORT);
1021         }
1022         mToast.setText(resid);
1023         mToast.show();
1024     }
1025
1026     private void startPlayback() {
1027
1028         if(mService == null)
1029             return;
1030         Intent intent = getIntent();
1031         String filename = "";
1032         Uri uri = intent.getData();
1033         if (uri != null && uri.toString().length() > 0) {
1034             // If this is a file:// URI, just use the path directly instead
1035             // of going through the open-from-filedescriptor codepath.
1036             String scheme = uri.getScheme();
1037             if ("file".equals(scheme)) {
1038                 filename = uri.getPath();
1039             } else {
1040                 filename = uri.toString();
1041             }
1042             try {
1043                 if (! ContentResolver.SCHEME_CONTENT.equals(scheme) ||
1044                         ! MediaStore.AUTHORITY.equals(uri.getAuthority())) {
1045                     mOneShot = true;
1046                 }
1047                 mService.stop();
1048                 mService.openFile(filename, mOneShot);
1049                 mService.play();
1050                 setIntent(new Intent());
1051             } catch (Exception ex) {
1052                 Log.d("MediaPlaybackActivity", "couldn't start playback: " + ex);
1053             }
1054         }
1055
1056         updateTrackInfo();
1057         long next = refreshNow();
1058         queueNextRefresh(next);
1059     }
1060
1061     private ServiceConnection osc = new ServiceConnection() {
1062             public void onServiceConnected(ComponentName classname, IBinder obj) {
1063                 mService = IMediaPlaybackService.Stub.asInterface(obj);
1064                 startPlayback();
1065                 try {
1066                     // Assume something is playing when the service says it is,
1067                     // but also if the audio ID is valid but the service is paused.
1068                     if (mService.getAudioId() >= 0 || mService.isPlaying() ||
1069                             mService.getPath() != null) {
1070                         // something is playing now, we're done
1071                         if (mOneShot || mService.getAudioId() < 0) {
1072                             mRepeatButton.setVisibility(View.INVISIBLE);
1073                             mShuffleButton.setVisibility(View.INVISIBLE);
1074                             mQueueButton.setVisibility(View.INVISIBLE);
1075                         } else {
1076                             mRepeatButton.setVisibility(View.VISIBLE);
1077                             mShuffleButton.setVisibility(View.VISIBLE);
1078                             mQueueButton.setVisibility(View.VISIBLE);
1079                             setRepeatButtonImage();
1080                             setShuffleButtonImage();
1081                         }
1082                         setPauseButtonImage();
1083                         return;
1084                     }
1085                 } catch (RemoteException ex) {
1086                 }
1087                 // Service is dead or not playing anything. If we got here as part
1088                 // of a "play this file" Intent, exit. Otherwise go to the Music
1089                 // app start screen.
1090                 if (getIntent().getData() == null) {
1091                     Intent intent = new Intent(Intent.ACTION_MAIN);
1092                     intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1093                     intent.setClass(MediaPlaybackActivity.this, MusicBrowserActivity.class);
1094                     startActivity(intent);
1095                 }
1096                 finish();
1097             }
1098             public void onServiceDisconnected(ComponentName classname) {
1099             }
1100     };
1101
1102     private void setRepeatButtonImage() {
1103         try {
1104             switch (mService.getRepeatMode()) {
1105                 case MediaPlaybackService.REPEAT_ALL:
1106                     mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_all_btn);
1107                     break;
1108                 case MediaPlaybackService.REPEAT_CURRENT:
1109                     mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_once_btn);
1110                     break;
1111                 default:
1112                     mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_off_btn);
1113                     break;
1114             }
1115         } catch (RemoteException ex) {
1116         }
1117     }
1118     
1119     private void setShuffleButtonImage() {
1120         try {
1121             switch (mService.getShuffleMode()) {
1122                 case MediaPlaybackService.SHUFFLE_NONE:
1123                     mShuffleButton.setImageResource(R.drawable.ic_mp_shuffle_off_btn);
1124                     break;
1125                 case MediaPlaybackService.SHUFFLE_AUTO:
1126                     mShuffleButton.setImageResource(R.drawable.ic_mp_partyshuffle_on_btn);
1127                     break;
1128                 default:
1129                     mShuffleButton.setImageResource(R.drawable.ic_mp_shuffle_on_btn);
1130                     break;
1131             }
1132         } catch (RemoteException ex) {
1133         }
1134     }
1135     
1136     private void setPauseButtonImage() {
1137         try {
1138             if (mService != null && mService.isPlaying()) {
1139                 mPauseButton.setImageResource(android.R.drawable.ic_media_pause);
1140             } else {
1141                 mPauseButton.setImageResource(android.R.drawable.ic_media_play);
1142             }
1143         } catch (RemoteException ex) {
1144         }
1145     }
1146     
1147     private ImageView mAlbum;
1148     private TextView mCurrentTime;
1149     private TextView mTotalTime;
1150     private TextView mArtistName;
1151     private TextView mAlbumName;
1152     private TextView mTrackName;
1153     private ProgressBar mProgress;
1154     private long mPosOverride = -1;
1155     private boolean mFromTouch = false;
1156     private long mDuration;
1157     private int seekmethod;
1158     private boolean paused;
1159
1160     private static final int REFRESH = 1;
1161     private static final int QUIT = 2;
1162     private static final int GET_ALBUM_ART = 3;
1163     private static final int ALBUM_ART_DECODED = 4;
1164
1165     private void queueNextRefresh(long delay) {
1166         if (!paused) {
1167             Message msg = mHandler.obtainMessage(REFRESH);
1168             mHandler.removeMessages(REFRESH);
1169             mHandler.sendMessageDelayed(msg, delay);
1170         }
1171     }
1172
1173     private long refreshNow() {
1174         if(mService == null)
1175             return 500;
1176         try {
1177             long pos = mPosOverride < 0 ? mService.position() : mPosOverride;
1178             long remaining = 1000 - (pos % 1000);
1179             if ((pos >= 0) && (mDuration > 0)) {
1180                 mCurrentTime.setText(MusicUtils.makeTimeString(this, pos / 1000));
1181                 
1182                 if (mService.isPlaying()) {
1183                     mCurrentTime.setVisibility(View.VISIBLE);
1184                 } else {
1185                     // blink the counter
1186                     int vis = mCurrentTime.getVisibility();
1187                     mCurrentTime.setVisibility(vis == View.INVISIBLE ? View.VISIBLE : View.INVISIBLE);
1188                     remaining = 500;
1189                 }
1190
1191                 mProgress.setProgress((int) (1000 * pos / mDuration));
1192             } else {
1193                 mCurrentTime.setText("--:--");
1194                 mProgress.setProgress(1000);
1195             }
1196             // return the number of milliseconds until the next full second, so
1197             // the counter can be updated at just the right time
1198             return remaining;
1199         } catch (RemoteException ex) {
1200         }
1201         return 500;
1202     }
1203     
1204     private final Handler mHandler = new Handler() {
1205         @Override
1206         public void handleMessage(Message msg) {
1207             switch (msg.what) {
1208                 case ALBUM_ART_DECODED:
1209                     mAlbum.setImageBitmap((Bitmap)msg.obj);
1210                     mAlbum.getDrawable().setDither(true);
1211                     break;
1212
1213                 case REFRESH:
1214                     long next = refreshNow();
1215                     queueNextRefresh(next);
1216                     break;
1217                     
1218                 case QUIT:
1219                     // This can be moved back to onCreate once the bug that prevents
1220                     // Dialogs from being started from onCreate/onResume is fixed.
1221                     new AlertDialog.Builder(MediaPlaybackActivity.this)
1222                             .setTitle(R.string.service_start_error_title)
1223                             .setMessage(R.string.service_start_error_msg)
1224                             .setPositiveButton(R.string.service_start_error_button,
1225                                     new DialogInterface.OnClickListener() {
1226                                         public void onClick(DialogInterface dialog, int whichButton) {
1227                                             finish();
1228                                         }
1229                                     })
1230                             .setCancelable(false)
1231                             .show();
1232                     break;
1233
1234                 default:
1235                     break;
1236             }
1237         }
1238     };
1239
1240     private BroadcastReceiver mStatusListener = new BroadcastReceiver() {
1241         @Override
1242         public void onReceive(Context context, Intent intent) {
1243             String action = intent.getAction();
1244             if (action.equals(MediaPlaybackService.META_CHANGED)) {
1245                 // redraw the artist/title info and
1246                 // set new max for progress bar
1247                 updateTrackInfo();
1248                 setPauseButtonImage();
1249                 queueNextRefresh(1);
1250             } else if (action.equals(MediaPlaybackService.PLAYBACK_COMPLETE)) {
1251                 if (mOneShot) {
1252                     finish();
1253                 } else {
1254                     setPauseButtonImage();
1255                 }
1256             } else if (action.equals(MediaPlaybackService.PLAYSTATE_CHANGED)) {
1257                 setPauseButtonImage();
1258             }
1259         }
1260     };
1261
1262     private static class AlbumSongIdWrapper {
1263         public long albumid;
1264         public long songid;
1265         AlbumSongIdWrapper(long aid, long sid) {
1266             albumid = aid;
1267             songid = sid;
1268         }
1269     }
1270     
1271     private void updateTrackInfo() {
1272         if (mService == null) {
1273             return;
1274         }
1275         try {
1276             String path = mService.getPath();
1277             if (path == null) {
1278                 finish();
1279                 return;
1280             }
1281             
1282             long songid = mService.getAudioId(); 
1283             if (songid < 0 && path.toLowerCase().startsWith("http://")) {
1284                 // Once we can get album art and meta data from MediaPlayer, we
1285                 // can show that info again when streaming.
1286                 ((View) mArtistName.getParent()).setVisibility(View.INVISIBLE);
1287                 ((View) mAlbumName.getParent()).setVisibility(View.INVISIBLE);
1288                 mAlbum.setVisibility(View.GONE);
1289                 mTrackName.setText(path);
1290                 mAlbumArtHandler.removeMessages(GET_ALBUM_ART);
1291                 mAlbumArtHandler.obtainMessage(GET_ALBUM_ART, new AlbumSongIdWrapper(-1, -1)).sendToTarget();
1292             } else {
1293                 ((View) mArtistName.getParent()).setVisibility(View.VISIBLE);
1294                 ((View) mAlbumName.getParent()).setVisibility(View.VISIBLE);
1295                 String artistName = mService.getArtistName();
1296                 if (MediaFile.UNKNOWN_STRING.equals(artistName)) {
1297                     artistName = getString(R.string.unknown_artist_name);
1298                 }
1299                 mArtistName.setText(artistName);
1300                 String albumName = mService.getAlbumName();
1301                 long albumid = mService.getAlbumId();
1302                 if (MediaFile.UNKNOWN_STRING.equals(albumName)) {
1303                     albumName = getString(R.string.unknown_album_name);
1304                     albumid = -1;
1305                 }
1306                 mAlbumName.setText(albumName);
1307                 mTrackName.setText(mService.getTrackName());
1308                 mAlbumArtHandler.removeMessages(GET_ALBUM_ART);
1309                 mAlbumArtHandler.obtainMessage(GET_ALBUM_ART, new AlbumSongIdWrapper(albumid, songid)).sendToTarget();
1310                 mAlbum.setVisibility(View.VISIBLE);
1311             }
1312             mDuration = mService.duration();
1313             mTotalTime.setText(MusicUtils.makeTimeString(this, mDuration / 1000));
1314         } catch (RemoteException ex) {
1315             finish();
1316         }
1317     }
1318     
1319     public class AlbumArtHandler extends Handler {
1320         private long mAlbumId = -1;
1321         
1322         public AlbumArtHandler(Looper looper) {
1323             super(looper);
1324         }
1325         @Override
1326         public void handleMessage(Message msg)
1327         {
1328             long albumid = ((AlbumSongIdWrapper) msg.obj).albumid;
1329             long songid = ((AlbumSongIdWrapper) msg.obj).songid;
1330             if (msg.what == GET_ALBUM_ART && (mAlbumId != albumid || albumid < 0)) {
1331                 // while decoding the new image, show the default album art
1332                 Message numsg = mHandler.obtainMessage(ALBUM_ART_DECODED, null);
1333                 mHandler.removeMessages(ALBUM_ART_DECODED);
1334                 mHandler.sendMessageDelayed(numsg, 300);
1335                 Bitmap bm = MusicUtils.getArtwork(MediaPlaybackActivity.this, songid, albumid);
1336                 if (bm == null) {
1337                     bm = MusicUtils.getArtwork(MediaPlaybackActivity.this, songid, -1);
1338                     albumid = -1;
1339                 }
1340                 if (bm != null) {
1341                     numsg = mHandler.obtainMessage(ALBUM_ART_DECODED, bm);
1342                     mHandler.removeMessages(ALBUM_ART_DECODED);
1343                     mHandler.sendMessage(numsg);
1344                 }
1345                 mAlbumId = albumid;
1346             }
1347         }
1348     }
1349     
1350     private static class Worker implements Runnable {
1351         private final Object mLock = new Object();
1352         private Looper mLooper;
1353         
1354         /**
1355          * Creates a worker thread with the given name. The thread
1356          * then runs a {@link android.os.Looper}.
1357          * @param name A name for the new thread
1358          */
1359         Worker(String name) {
1360             Thread t = new Thread(null, this, name);
1361             t.setPriority(Thread.MIN_PRIORITY);
1362             t.start();
1363             synchronized (mLock) {
1364                 while (mLooper == null) {
1365                     try {
1366                         mLock.wait();
1367                     } catch (InterruptedException ex) {
1368                     }
1369                 }
1370             }
1371         }
1372         
1373         public Looper getLooper() {
1374             return mLooper;
1375         }
1376         
1377         public void run() {
1378             synchronized (mLock) {
1379                 Looper.prepare();
1380                 mLooper = Looper.myLooper();
1381                 mLock.notifyAll();
1382             }
1383             Looper.loop();
1384         }
1385         
1386         public void quit() {
1387             mLooper.quit();
1388         }
1389     }
1390 }
1391