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