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