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