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