MediaPlaybackActivity.java revision f33a575c2b09095d58ad7af527113f767e4c37b1
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.res.Configuration;
35import android.database.Cursor;
36import android.graphics.Bitmap;
37import android.media.AudioManager;
38import android.net.Uri;
39import android.os.Bundle;
40import android.os.Handler;
41import android.os.IBinder;
42import android.os.Looper;
43import android.os.Message;
44import android.os.RemoteException;
45import android.os.SystemClock;
46import android.provider.MediaStore;
47import android.text.Layout;
48import android.text.TextUtils.TruncateAt;
49import android.util.Log;
50import android.view.KeyEvent;
51import android.view.Menu;
52import android.view.MenuItem;
53import android.view.MotionEvent;
54import android.view.SubMenu;
55import android.view.View;
56import android.view.ViewConfiguration;
57import android.view.Window;
58import android.widget.ImageButton;
59import android.widget.ImageView;
60import android.widget.ProgressBar;
61import android.widget.SeekBar;
62import android.widget.TextView;
63import android.widget.Toast;
64import android.widget.SeekBar.OnSeekBarChangeListener;
65
66
67public class MediaPlaybackActivity extends Activity implements MusicUtils.Defs,
68    View.OnTouchListener, View.OnLongClickListener
69{
70    private static final int USE_AS_RINGTONE = CHILD_MENU_BASE;
71
72    private boolean mOneShot = false;
73    private boolean mSeeking = false;
74    private boolean mDeviceHasDpad;
75    private long mStartSeekPos = 0;
76    private long mLastSeekEventTime;
77    private IMediaPlaybackService mService = null;
78    private RepeatingImageButton mPrevButton;
79    private ImageButton mPauseButton;
80    private RepeatingImageButton mNextButton;
81    private ImageButton mRepeatButton;
82    private ImageButton mShuffleButton;
83    private ImageButton mQueueButton;
84    private Worker mAlbumArtWorker;
85    private AlbumArtHandler mAlbumArtHandler;
86    private Toast mToast;
87    private int mTouchSlop;
88    private ServiceToken mToken;
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 (MediaStore.UNKNOWN_STRING.equals(album) &&
291                MediaStore.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) && !MediaStore.UNKNOWN_STRING.equals(artist);
318
319        boolean knownalbum =
320            (album != null) && !MediaStore.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) || MediaStore.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(mToken);
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        mToken = MusicUtils.bindToService(this, osc);
494        if (mToken == null) {
495            // something went wrong
496            mHandler.sendEmptyMessage(QUIT);
497        }
498
499        IntentFilter f = new IntentFilter();
500        f.addAction(MediaPlaybackService.PLAYSTATE_CHANGED);
501        f.addAction(MediaPlaybackService.META_CHANGED);
502        f.addAction(MediaPlaybackService.PLAYBACK_COMPLETE);
503        registerReceiver(mStatusListener, new IntentFilter(f));
504        updateTrackInfo();
505        long next = refreshNow();
506        queueNextRefresh(next);
507    }
508
509    @Override
510    public void onNewIntent(Intent intent) {
511        setIntent(intent);
512        mOneShot = intent.getBooleanExtra("oneshot", false);
513    }
514
515    @Override
516    public void onResume() {
517        super.onResume();
518        updateTrackInfo();
519        setPauseButtonImage();
520    }
521
522    @Override
523    public void onDestroy()
524    {
525        mAlbumArtWorker.quit();
526        super.onDestroy();
527        //System.out.println("***************** playback activity onDestroy\n");
528    }
529
530    @Override
531    public boolean onCreateOptionsMenu(Menu menu) {
532        super.onCreateOptionsMenu(menu);
533        // Don't show the menu items if we got launched by path/filedescriptor, or
534        // if we're in one shot mode. In most cases, these menu items are not
535        // useful in those modes, so for consistency we never show them in these
536        // modes, instead of tailoring them to the specific file being played.
537        if (MusicUtils.getCurrentAudioId() >= 0 && !mOneShot) {
538            menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library);
539            menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu()
540            SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0,
541                    R.string.add_to_playlist).setIcon(android.R.drawable.ic_menu_add);
542            // these next two are in a separate group, so they can be shown/hidden as needed
543            // based on the keyguard state
544            menu.add(1, USE_AS_RINGTONE, 0, R.string.ringtone_menu_short)
545                    .setIcon(R.drawable.ic_menu_set_as_ringtone);
546            menu.add(1, DELETE_ITEM, 0, R.string.delete_item)
547                    .setIcon(R.drawable.ic_menu_delete);
548            return true;
549        }
550        return false;
551    }
552
553    @Override
554    public boolean onPrepareOptionsMenu(Menu menu) {
555        MenuItem item = menu.findItem(PARTY_SHUFFLE);
556        if (item != null) {
557            int shuffle = MusicUtils.getCurrentShuffleMode();
558            if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
559                item.setIcon(R.drawable.ic_menu_party_shuffle);
560                item.setTitle(R.string.party_shuffle_off);
561            } else {
562                item.setIcon(R.drawable.ic_menu_party_shuffle);
563                item.setTitle(R.string.party_shuffle);
564            }
565        }
566
567        item = menu.findItem(ADD_TO_PLAYLIST);
568        if (item != null) {
569            SubMenu sub = item.getSubMenu();
570            MusicUtils.makePlaylistMenu(this, sub);
571        }
572
573        KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
574        menu.setGroupVisible(1, !km.inKeyguardRestrictedInputMode());
575
576        return true;
577    }
578
579    @Override
580    public boolean onOptionsItemSelected(MenuItem item) {
581        Intent intent;
582        try {
583            switch (item.getItemId()) {
584                case GOTO_START:
585                    intent = new Intent();
586                    intent.setClass(this, MusicBrowserActivity.class);
587                    startActivity(intent);
588                    finish();
589                    break;
590                case USE_AS_RINGTONE: {
591                    // Set the system setting to make this the current ringtone
592                    if (mService != null) {
593                        MusicUtils.setRingtone(this, mService.getAudioId());
594                    }
595                    return true;
596                }
597                case PARTY_SHUFFLE:
598                    MusicUtils.togglePartyShuffle();
599                    setShuffleButtonImage();
600                    break;
601
602                case NEW_PLAYLIST: {
603                    intent = new Intent();
604                    intent.setClass(this, CreatePlaylist.class);
605                    startActivityForResult(intent, NEW_PLAYLIST);
606                    return true;
607                }
608
609                case PLAYLIST_SELECTED: {
610                    long [] list = new long[1];
611                    list[0] = MusicUtils.getCurrentAudioId();
612                    long playlist = item.getIntent().getLongExtra("playlist", 0);
613                    MusicUtils.addToPlaylist(this, list, playlist);
614                    return true;
615                }
616
617                case DELETE_ITEM: {
618                    if (mService != null) {
619                        long [] list = new long[1];
620                        list[0] = MusicUtils.getCurrentAudioId();
621                        Bundle b = new Bundle();
622                        b.putString("description", getString(R.string.delete_song_desc,
623                                mService.getTrackName()));
624                        b.putLongArray("items", list);
625                        intent = new Intent();
626                        intent.setClass(this, DeleteItems.class);
627                        intent.putExtras(b);
628                        startActivityForResult(intent, -1);
629                    }
630                    return true;
631                }
632            }
633        } catch (RemoteException ex) {
634        }
635        return super.onOptionsItemSelected(item);
636    }
637
638    @Override
639    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
640        if (resultCode != RESULT_OK) {
641            return;
642        }
643        switch (requestCode) {
644            case NEW_PLAYLIST:
645                Uri uri = intent.getData();
646                if (uri != null) {
647                    long [] list = new long[1];
648                    list[0] = MusicUtils.getCurrentAudioId();
649                    int playlist = Integer.parseInt(uri.getLastPathSegment());
650                    MusicUtils.addToPlaylist(this, list, playlist);
651                }
652                break;
653        }
654    }
655    private final int keyboard[][] = {
656        {
657            KeyEvent.KEYCODE_Q,
658            KeyEvent.KEYCODE_W,
659            KeyEvent.KEYCODE_E,
660            KeyEvent.KEYCODE_R,
661            KeyEvent.KEYCODE_T,
662            KeyEvent.KEYCODE_Y,
663            KeyEvent.KEYCODE_U,
664            KeyEvent.KEYCODE_I,
665            KeyEvent.KEYCODE_O,
666            KeyEvent.KEYCODE_P,
667        },
668        {
669            KeyEvent.KEYCODE_A,
670            KeyEvent.KEYCODE_S,
671            KeyEvent.KEYCODE_D,
672            KeyEvent.KEYCODE_F,
673            KeyEvent.KEYCODE_G,
674            KeyEvent.KEYCODE_H,
675            KeyEvent.KEYCODE_J,
676            KeyEvent.KEYCODE_K,
677            KeyEvent.KEYCODE_L,
678            KeyEvent.KEYCODE_DEL,
679        },
680        {
681            KeyEvent.KEYCODE_Z,
682            KeyEvent.KEYCODE_X,
683            KeyEvent.KEYCODE_C,
684            KeyEvent.KEYCODE_V,
685            KeyEvent.KEYCODE_B,
686            KeyEvent.KEYCODE_N,
687            KeyEvent.KEYCODE_M,
688            KeyEvent.KEYCODE_COMMA,
689            KeyEvent.KEYCODE_PERIOD,
690            KeyEvent.KEYCODE_ENTER
691        }
692
693    };
694
695    private int lastX;
696    private int lastY;
697
698    private boolean seekMethod1(int keyCode)
699    {
700        if (mService == null) return false;
701        for(int x=0;x<10;x++) {
702            for(int y=0;y<3;y++) {
703                if(keyboard[y][x] == keyCode) {
704                    int dir = 0;
705                    // top row
706                    if(x == lastX && y == lastY) dir = 0;
707                    else if (y == 0 && lastY == 0 && x > lastX) dir = 1;
708                    else if (y == 0 && lastY == 0 && x < lastX) dir = -1;
709                    // bottom row
710                    else if (y == 2 && lastY == 2 && x > lastX) dir = -1;
711                    else if (y == 2 && lastY == 2 && x < lastX) dir = 1;
712                    // moving up
713                    else if (y < lastY && x <= 4) dir = 1;
714                    else if (y < lastY && x >= 5) dir = -1;
715                    // moving down
716                    else if (y > lastY && x <= 4) dir = -1;
717                    else if (y > lastY && x >= 5) dir = 1;
718                    lastX = x;
719                    lastY = y;
720                    try {
721                        mService.seek(mService.position() + dir * 5);
722                    } catch (RemoteException ex) {
723                    }
724                    refreshNow();
725                    return true;
726                }
727            }
728        }
729        lastX = -1;
730        lastY = -1;
731        return false;
732    }
733
734    private boolean seekMethod2(int keyCode)
735    {
736        if (mService == null) return false;
737        for(int i=0;i<10;i++) {
738            if(keyboard[0][i] == keyCode) {
739                int seekpercentage = 100*i/10;
740                try {
741                    mService.seek(mService.duration() * seekpercentage / 100);
742                } catch (RemoteException ex) {
743                }
744                refreshNow();
745                return true;
746            }
747        }
748        return false;
749    }
750
751    @Override
752    public boolean onKeyUp(int keyCode, KeyEvent event) {
753        try {
754            switch(keyCode)
755            {
756                case KeyEvent.KEYCODE_DPAD_LEFT:
757                    if (!useDpadMusicControl()) {
758                        break;
759                    }
760                    if (mService != null) {
761                        if (!mSeeking && mStartSeekPos >= 0) {
762                            mPauseButton.requestFocus();
763                            if (mStartSeekPos < 1000) {
764                                mService.prev();
765                            } else {
766                                mService.seek(0);
767                            }
768                        } else {
769                            scanBackward(-1, event.getEventTime() - event.getDownTime());
770                            mPauseButton.requestFocus();
771                            mStartSeekPos = -1;
772                        }
773                    }
774                    mSeeking = false;
775                    mPosOverride = -1;
776                    return true;
777                case KeyEvent.KEYCODE_DPAD_RIGHT:
778                    if (!useDpadMusicControl()) {
779                        break;
780                    }
781                    if (mService != null) {
782                        if (!mSeeking && mStartSeekPos >= 0) {
783                            mPauseButton.requestFocus();
784                            mService.next();
785                        } else {
786                            scanForward(-1, event.getEventTime() - event.getDownTime());
787                            mPauseButton.requestFocus();
788                            mStartSeekPos = -1;
789                        }
790                    }
791                    mSeeking = false;
792                    mPosOverride = -1;
793                    return true;
794            }
795        } catch (RemoteException ex) {
796        }
797        return super.onKeyUp(keyCode, event);
798    }
799
800    private boolean useDpadMusicControl() {
801        if (mDeviceHasDpad && (mPrevButton.isFocused() ||
802                mNextButton.isFocused() ||
803                mPauseButton.isFocused())) {
804            return true;
805        }
806        return false;
807    }
808
809    @Override
810    public boolean onKeyDown(int keyCode, KeyEvent event)
811    {
812        int direction = -1;
813        int repcnt = event.getRepeatCount();
814
815        if((seekmethod==0)?seekMethod1(keyCode):seekMethod2(keyCode))
816            return true;
817
818        switch(keyCode)
819        {
820/*
821            // image scale
822            case KeyEvent.KEYCODE_Q: av.adjustParams(-0.05, 0.0, 0.0, 0.0, 0.0,-1.0); break;
823            case KeyEvent.KEYCODE_E: av.adjustParams( 0.05, 0.0, 0.0, 0.0, 0.0, 1.0); break;
824            // image translate
825            case KeyEvent.KEYCODE_W: av.adjustParams(    0.0, 0.0,-1.0, 0.0, 0.0, 0.0); break;
826            case KeyEvent.KEYCODE_X: av.adjustParams(    0.0, 0.0, 1.0, 0.0, 0.0, 0.0); break;
827            case KeyEvent.KEYCODE_A: av.adjustParams(    0.0,-1.0, 0.0, 0.0, 0.0, 0.0); break;
828            case KeyEvent.KEYCODE_D: av.adjustParams(    0.0, 1.0, 0.0, 0.0, 0.0, 0.0); break;
829            // camera rotation
830            case KeyEvent.KEYCODE_R: av.adjustParams(    0.0, 0.0, 0.0, 0.0, 0.0,-1.0); break;
831            case KeyEvent.KEYCODE_U: av.adjustParams(    0.0, 0.0, 0.0, 0.0, 0.0, 1.0); break;
832            // camera translate
833            case KeyEvent.KEYCODE_Y: av.adjustParams(    0.0, 0.0, 0.0, 0.0,-1.0, 0.0); break;
834            case KeyEvent.KEYCODE_N: av.adjustParams(    0.0, 0.0, 0.0, 0.0, 1.0, 0.0); break;
835            case KeyEvent.KEYCODE_G: av.adjustParams(    0.0, 0.0, 0.0,-1.0, 0.0, 0.0); break;
836            case KeyEvent.KEYCODE_J: av.adjustParams(    0.0, 0.0, 0.0, 1.0, 0.0, 0.0); break;
837
838*/
839
840            case KeyEvent.KEYCODE_SLASH:
841                seekmethod = 1 - seekmethod;
842                return true;
843
844            case KeyEvent.KEYCODE_DPAD_LEFT:
845                if (!useDpadMusicControl()) {
846                    break;
847                }
848                if (!mPrevButton.hasFocus()) {
849                    mPrevButton.requestFocus();
850                }
851                scanBackward(repcnt, event.getEventTime() - event.getDownTime());
852                return true;
853            case KeyEvent.KEYCODE_DPAD_RIGHT:
854                if (!useDpadMusicControl()) {
855                    break;
856                }
857                if (!mNextButton.hasFocus()) {
858                    mNextButton.requestFocus();
859                }
860                scanForward(repcnt, event.getEventTime() - event.getDownTime());
861                return true;
862
863            case KeyEvent.KEYCODE_S:
864                toggleShuffle();
865                return true;
866
867            case KeyEvent.KEYCODE_DPAD_CENTER:
868            case KeyEvent.KEYCODE_SPACE:
869                doPauseResume();
870                return true;
871        }
872        return super.onKeyDown(keyCode, event);
873    }
874
875    private void scanBackward(int repcnt, long delta) {
876        if(mService == null) return;
877        try {
878            if(repcnt == 0) {
879                mStartSeekPos = mService.position();
880                mLastSeekEventTime = 0;
881                mSeeking = false;
882            } else {
883                mSeeking = true;
884                if (delta < 5000) {
885                    // seek at 10x speed for the first 5 seconds
886                    delta = delta * 10;
887                } else {
888                    // seek at 40x after that
889                    delta = 50000 + (delta - 5000) * 40;
890                }
891                long newpos = mStartSeekPos - delta;
892                if (newpos < 0) {
893                    // move to previous track
894                    mService.prev();
895                    long duration = mService.duration();
896                    mStartSeekPos += duration;
897                    newpos += duration;
898                }
899                if (((delta - mLastSeekEventTime) > 250) || repcnt < 0){
900                    mService.seek(newpos);
901                    mLastSeekEventTime = delta;
902                }
903                if (repcnt >= 0) {
904                    mPosOverride = newpos;
905                } else {
906                    mPosOverride = -1;
907                }
908                refreshNow();
909            }
910        } catch (RemoteException ex) {
911        }
912    }
913
914    private void scanForward(int repcnt, long delta) {
915        if(mService == null) return;
916        try {
917            if(repcnt == 0) {
918                mStartSeekPos = mService.position();
919                mLastSeekEventTime = 0;
920                mSeeking = false;
921            } else {
922                mSeeking = true;
923                if (delta < 5000) {
924                    // seek at 10x speed for the first 5 seconds
925                    delta = delta * 10;
926                } else {
927                    // seek at 40x after that
928                    delta = 50000 + (delta - 5000) * 40;
929                }
930                long newpos = mStartSeekPos + delta;
931                long duration = mService.duration();
932                if (newpos >= duration) {
933                    // move to next track
934                    mService.next();
935                    mStartSeekPos -= duration; // is OK to go negative
936                    newpos -= duration;
937                }
938                if (((delta - mLastSeekEventTime) > 250) || repcnt < 0){
939                    mService.seek(newpos);
940                    mLastSeekEventTime = delta;
941                }
942                if (repcnt >= 0) {
943                    mPosOverride = newpos;
944                } else {
945                    mPosOverride = -1;
946                }
947                refreshNow();
948            }
949        } catch (RemoteException ex) {
950        }
951    }
952
953    private void doPauseResume() {
954        try {
955            if(mService != null) {
956                if (mService.isPlaying()) {
957                    mService.pause();
958                } else {
959                    mService.play();
960                }
961                refreshNow();
962                setPauseButtonImage();
963            }
964        } catch (RemoteException ex) {
965        }
966    }
967
968    private void toggleShuffle() {
969        if (mService == null) {
970            return;
971        }
972        try {
973            int shuffle = mService.getShuffleMode();
974            if (shuffle == MediaPlaybackService.SHUFFLE_NONE) {
975                mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
976                if (mService.getRepeatMode() == MediaPlaybackService.REPEAT_CURRENT) {
977                    mService.setRepeatMode(MediaPlaybackService.REPEAT_ALL);
978                    setRepeatButtonImage();
979                }
980                showToast(R.string.shuffle_on_notif);
981            } else if (shuffle == MediaPlaybackService.SHUFFLE_NORMAL ||
982                    shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
983                mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
984                showToast(R.string.shuffle_off_notif);
985            } else {
986                Log.e("MediaPlaybackActivity", "Invalid shuffle mode: " + shuffle);
987            }
988            setShuffleButtonImage();
989        } catch (RemoteException ex) {
990        }
991    }
992
993    private void cycleRepeat() {
994        if (mService == null) {
995            return;
996        }
997        try {
998            int mode = mService.getRepeatMode();
999            if (mode == MediaPlaybackService.REPEAT_NONE) {
1000                mService.setRepeatMode(MediaPlaybackService.REPEAT_ALL);
1001                showToast(R.string.repeat_all_notif);
1002            } else if (mode == MediaPlaybackService.REPEAT_ALL) {
1003                mService.setRepeatMode(MediaPlaybackService.REPEAT_CURRENT);
1004                if (mService.getShuffleMode() != MediaPlaybackService.SHUFFLE_NONE) {
1005                    mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
1006                    setShuffleButtonImage();
1007                }
1008                showToast(R.string.repeat_current_notif);
1009            } else {
1010                mService.setRepeatMode(MediaPlaybackService.REPEAT_NONE);
1011                showToast(R.string.repeat_off_notif);
1012            }
1013            setRepeatButtonImage();
1014        } catch (RemoteException ex) {
1015        }
1016
1017    }
1018
1019    private void showToast(int resid) {
1020        if (mToast == null) {
1021            mToast = Toast.makeText(this, "", Toast.LENGTH_SHORT);
1022        }
1023        mToast.setText(resid);
1024        mToast.show();
1025    }
1026
1027    private void startPlayback() {
1028
1029        if(mService == null)
1030            return;
1031        Intent intent = getIntent();
1032        String filename = "";
1033        Uri uri = intent.getData();
1034        if (uri != null && uri.toString().length() > 0) {
1035            // If this is a file:// URI, just use the path directly instead
1036            // of going through the open-from-filedescriptor codepath.
1037            String scheme = uri.getScheme();
1038            if ("file".equals(scheme)) {
1039                filename = uri.getPath();
1040            } else {
1041                filename = uri.toString();
1042            }
1043            try {
1044                if (! ContentResolver.SCHEME_CONTENT.equals(scheme) ||
1045                        ! MediaStore.AUTHORITY.equals(uri.getAuthority())) {
1046                    mOneShot = true;
1047                }
1048                mService.stop();
1049                mService.openFile(filename, mOneShot);
1050                mService.play();
1051                setIntent(new Intent());
1052            } catch (Exception ex) {
1053                Log.d("MediaPlaybackActivity", "couldn't start playback: " + ex);
1054            }
1055        }
1056
1057        updateTrackInfo();
1058        long next = refreshNow();
1059        queueNextRefresh(next);
1060    }
1061
1062    private ServiceConnection osc = new ServiceConnection() {
1063            public void onServiceConnected(ComponentName classname, IBinder obj) {
1064                mService = IMediaPlaybackService.Stub.asInterface(obj);
1065                startPlayback();
1066                try {
1067                    // Assume something is playing when the service says it is,
1068                    // but also if the audio ID is valid but the service is paused.
1069                    if (mService.getAudioId() >= 0 || mService.isPlaying() ||
1070                            mService.getPath() != null) {
1071                        // something is playing now, we're done
1072                        if (mOneShot || mService.getAudioId() < 0) {
1073                            mRepeatButton.setVisibility(View.INVISIBLE);
1074                            mShuffleButton.setVisibility(View.INVISIBLE);
1075                            mQueueButton.setVisibility(View.INVISIBLE);
1076                        } else {
1077                            mRepeatButton.setVisibility(View.VISIBLE);
1078                            mShuffleButton.setVisibility(View.VISIBLE);
1079                            mQueueButton.setVisibility(View.VISIBLE);
1080                            setRepeatButtonImage();
1081                            setShuffleButtonImage();
1082                        }
1083                        setPauseButtonImage();
1084                        return;
1085                    }
1086                } catch (RemoteException ex) {
1087                }
1088                // Service is dead or not playing anything. If we got here as part
1089                // of a "play this file" Intent, exit. Otherwise go to the Music
1090                // app start screen.
1091                if (getIntent().getData() == null) {
1092                    Intent intent = new Intent(Intent.ACTION_MAIN);
1093                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1094                    intent.setClass(MediaPlaybackActivity.this, MusicBrowserActivity.class);
1095                    startActivity(intent);
1096                }
1097                finish();
1098            }
1099            public void onServiceDisconnected(ComponentName classname) {
1100            }
1101    };
1102
1103    private void setRepeatButtonImage() {
1104        try {
1105            switch (mService.getRepeatMode()) {
1106                case MediaPlaybackService.REPEAT_ALL:
1107                    mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_all_btn);
1108                    break;
1109                case MediaPlaybackService.REPEAT_CURRENT:
1110                    mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_once_btn);
1111                    break;
1112                default:
1113                    mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_off_btn);
1114                    break;
1115            }
1116        } catch (RemoteException ex) {
1117        }
1118    }
1119
1120    private void setShuffleButtonImage() {
1121        try {
1122            switch (mService.getShuffleMode()) {
1123                case MediaPlaybackService.SHUFFLE_NONE:
1124                    mShuffleButton.setImageResource(R.drawable.ic_mp_shuffle_off_btn);
1125                    break;
1126                case MediaPlaybackService.SHUFFLE_AUTO:
1127                    mShuffleButton.setImageResource(R.drawable.ic_mp_partyshuffle_on_btn);
1128                    break;
1129                default:
1130                    mShuffleButton.setImageResource(R.drawable.ic_mp_shuffle_on_btn);
1131                    break;
1132            }
1133        } catch (RemoteException ex) {
1134        }
1135    }
1136
1137    private void setPauseButtonImage() {
1138        try {
1139            if (mService != null && mService.isPlaying()) {
1140                mPauseButton.setImageResource(android.R.drawable.ic_media_pause);
1141            } else {
1142                mPauseButton.setImageResource(android.R.drawable.ic_media_play);
1143            }
1144        } catch (RemoteException ex) {
1145        }
1146    }
1147
1148    private ImageView mAlbum;
1149    private TextView mCurrentTime;
1150    private TextView mTotalTime;
1151    private TextView mArtistName;
1152    private TextView mAlbumName;
1153    private TextView mTrackName;
1154    private ProgressBar mProgress;
1155    private long mPosOverride = -1;
1156    private boolean mFromTouch = false;
1157    private long mDuration;
1158    private int seekmethod;
1159    private boolean paused;
1160
1161    private static final int REFRESH = 1;
1162    private static final int QUIT = 2;
1163    private static final int GET_ALBUM_ART = 3;
1164    private static final int ALBUM_ART_DECODED = 4;
1165
1166    private void queueNextRefresh(long delay) {
1167        if (!paused) {
1168            Message msg = mHandler.obtainMessage(REFRESH);
1169            mHandler.removeMessages(REFRESH);
1170            mHandler.sendMessageDelayed(msg, delay);
1171        }
1172    }
1173
1174    private long refreshNow() {
1175        if(mService == null)
1176            return 500;
1177        try {
1178            long pos = mPosOverride < 0 ? mService.position() : mPosOverride;
1179            long remaining = 1000 - (pos % 1000);
1180            if ((pos >= 0) && (mDuration > 0)) {
1181                mCurrentTime.setText(MusicUtils.makeTimeString(this, pos / 1000));
1182
1183                if (mService.isPlaying()) {
1184                    mCurrentTime.setVisibility(View.VISIBLE);
1185                } else {
1186                    // blink the counter
1187                    int vis = mCurrentTime.getVisibility();
1188                    mCurrentTime.setVisibility(vis == View.INVISIBLE ? View.VISIBLE : View.INVISIBLE);
1189                    remaining = 500;
1190                }
1191
1192                mProgress.setProgress((int) (1000 * pos / mDuration));
1193            } else {
1194                mCurrentTime.setText("--:--");
1195                mProgress.setProgress(1000);
1196            }
1197            // return the number of milliseconds until the next full second, so
1198            // the counter can be updated at just the right time
1199            return remaining;
1200        } catch (RemoteException ex) {
1201        }
1202        return 500;
1203    }
1204
1205    private final Handler mHandler = new Handler() {
1206        @Override
1207        public void handleMessage(Message msg) {
1208            switch (msg.what) {
1209                case ALBUM_ART_DECODED:
1210                    mAlbum.setImageBitmap((Bitmap)msg.obj);
1211                    mAlbum.getDrawable().setDither(true);
1212                    break;
1213
1214                case REFRESH:
1215                    long next = refreshNow();
1216                    queueNextRefresh(next);
1217                    break;
1218
1219                case QUIT:
1220                    // This can be moved back to onCreate once the bug that prevents
1221                    // Dialogs from being started from onCreate/onResume is fixed.
1222                    new AlertDialog.Builder(MediaPlaybackActivity.this)
1223                            .setTitle(R.string.service_start_error_title)
1224                            .setMessage(R.string.service_start_error_msg)
1225                            .setPositiveButton(R.string.service_start_error_button,
1226                                    new DialogInterface.OnClickListener() {
1227                                        public void onClick(DialogInterface dialog, int whichButton) {
1228                                            finish();
1229                                        }
1230                                    })
1231                            .setCancelable(false)
1232                            .show();
1233                    break;
1234
1235                default:
1236                    break;
1237            }
1238        }
1239    };
1240
1241    private BroadcastReceiver mStatusListener = new BroadcastReceiver() {
1242        @Override
1243        public void onReceive(Context context, Intent intent) {
1244            String action = intent.getAction();
1245            if (action.equals(MediaPlaybackService.META_CHANGED)) {
1246                // redraw the artist/title info and
1247                // set new max for progress bar
1248                updateTrackInfo();
1249                setPauseButtonImage();
1250                queueNextRefresh(1);
1251            } else if (action.equals(MediaPlaybackService.PLAYBACK_COMPLETE)) {
1252                if (mOneShot) {
1253                    finish();
1254                } else {
1255                    setPauseButtonImage();
1256                }
1257            } else if (action.equals(MediaPlaybackService.PLAYSTATE_CHANGED)) {
1258                setPauseButtonImage();
1259            }
1260        }
1261    };
1262
1263    private static class AlbumSongIdWrapper {
1264        public long albumid;
1265        public long songid;
1266        AlbumSongIdWrapper(long aid, long sid) {
1267            albumid = aid;
1268            songid = sid;
1269        }
1270    }
1271
1272    private void updateTrackInfo() {
1273        if (mService == null) {
1274            return;
1275        }
1276        try {
1277            String path = mService.getPath();
1278            if (path == null) {
1279                finish();
1280                return;
1281            }
1282
1283            long songid = mService.getAudioId();
1284            if (songid < 0 && path.toLowerCase().startsWith("http://")) {
1285                // Once we can get album art and meta data from MediaPlayer, we
1286                // can show that info again when streaming.
1287                ((View) mArtistName.getParent()).setVisibility(View.INVISIBLE);
1288                ((View) mAlbumName.getParent()).setVisibility(View.INVISIBLE);
1289                mAlbum.setVisibility(View.GONE);
1290                mTrackName.setText(path);
1291                mAlbumArtHandler.removeMessages(GET_ALBUM_ART);
1292                mAlbumArtHandler.obtainMessage(GET_ALBUM_ART, new AlbumSongIdWrapper(-1, -1)).sendToTarget();
1293            } else {
1294                ((View) mArtistName.getParent()).setVisibility(View.VISIBLE);
1295                ((View) mAlbumName.getParent()).setVisibility(View.VISIBLE);
1296                String artistName = mService.getArtistName();
1297                if (MediaStore.UNKNOWN_STRING.equals(artistName)) {
1298                    artistName = getString(R.string.unknown_artist_name);
1299                }
1300                mArtistName.setText(artistName);
1301                String albumName = mService.getAlbumName();
1302                long albumid = mService.getAlbumId();
1303                if (MediaStore.UNKNOWN_STRING.equals(albumName)) {
1304                    albumName = getString(R.string.unknown_album_name);
1305                    albumid = -1;
1306                }
1307                mAlbumName.setText(albumName);
1308                mTrackName.setText(mService.getTrackName());
1309                mAlbumArtHandler.removeMessages(GET_ALBUM_ART);
1310                mAlbumArtHandler.obtainMessage(GET_ALBUM_ART, new AlbumSongIdWrapper(albumid, songid)).sendToTarget();
1311                mAlbum.setVisibility(View.VISIBLE);
1312            }
1313            mDuration = mService.duration();
1314            mTotalTime.setText(MusicUtils.makeTimeString(this, mDuration / 1000));
1315        } catch (RemoteException ex) {
1316            finish();
1317        }
1318    }
1319
1320    public class AlbumArtHandler extends Handler {
1321        private long mAlbumId = -1;
1322
1323        public AlbumArtHandler(Looper looper) {
1324            super(looper);
1325        }
1326        @Override
1327        public void handleMessage(Message msg)
1328        {
1329            long albumid = ((AlbumSongIdWrapper) msg.obj).albumid;
1330            long songid = ((AlbumSongIdWrapper) msg.obj).songid;
1331            if (msg.what == GET_ALBUM_ART && (mAlbumId != albumid || albumid < 0)) {
1332                // while decoding the new image, show the default album art
1333                Message numsg = mHandler.obtainMessage(ALBUM_ART_DECODED, null);
1334                mHandler.removeMessages(ALBUM_ART_DECODED);
1335                mHandler.sendMessageDelayed(numsg, 300);
1336                Bitmap bm = MusicUtils.getArtwork(MediaPlaybackActivity.this, songid, albumid);
1337                if (bm == null) {
1338                    bm = MusicUtils.getArtwork(MediaPlaybackActivity.this, songid, -1);
1339                    albumid = -1;
1340                }
1341                if (bm != null) {
1342                    numsg = mHandler.obtainMessage(ALBUM_ART_DECODED, bm);
1343                    mHandler.removeMessages(ALBUM_ART_DECODED);
1344                    mHandler.sendMessage(numsg);
1345                }
1346                mAlbumId = albumid;
1347            }
1348        }
1349    }
1350
1351    private static class Worker implements Runnable {
1352        private final Object mLock = new Object();
1353        private Looper mLooper;
1354
1355        /**
1356         * Creates a worker thread with the given name. The thread
1357         * then runs a {@link android.os.Looper}.
1358         * @param name A name for the new thread
1359         */
1360        Worker(String name) {
1361            Thread t = new Thread(null, this, name);
1362            t.setPriority(Thread.MIN_PRIORITY);
1363            t.start();
1364            synchronized (mLock) {
1365                while (mLooper == null) {
1366                    try {
1367                        mLock.wait();
1368                    } catch (InterruptedException ex) {
1369                    }
1370                }
1371            }
1372        }
1373
1374        public Looper getLooper() {
1375            return mLooper;
1376        }
1377
1378        public void run() {
1379            synchronized (mLock) {
1380                Looper.prepare();
1381                mLooper = Looper.myLooper();
1382                mLock.notifyAll();
1383            }
1384            Looper.loop();
1385        }
1386
1387        public void quit() {
1388            mLooper.quit();
1389        }
1390    }
1391}
1392
1393