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