1/*
2 * Copyright (C) 2010 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.content.AsyncQueryHandler;
21import android.content.ContentResolver;
22import android.content.Context;
23import android.content.Intent;
24import android.database.Cursor;
25import android.media.AudioManager;
26import android.media.MediaPlayer;
27import android.media.AudioManager.OnAudioFocusChangeListener;
28import android.media.MediaPlayer.OnCompletionListener;
29import android.media.MediaPlayer.OnErrorListener;
30import android.media.MediaPlayer.OnPreparedListener;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.Handler;
34import android.provider.MediaStore;
35import android.provider.OpenableColumns;
36import android.text.TextUtils;
37import android.util.Log;
38import android.view.KeyEvent;
39import android.view.Menu;
40import android.view.MenuItem;
41import android.view.View;
42import android.view.Window;
43import android.view.WindowManager;
44import android.widget.ImageButton;
45import android.widget.ProgressBar;
46import android.widget.SeekBar;
47import android.widget.TextView;
48import android.widget.SeekBar.OnSeekBarChangeListener;
49import android.widget.Toast;
50
51import java.io.IOException;
52
53/**
54 * Dialog that comes up in response to various music-related VIEW intents.
55 */
56public class AudioPreview extends Activity implements OnPreparedListener, OnErrorListener, OnCompletionListener
57{
58    private final static String TAG = "AudioPreview";
59    private PreviewPlayer mPlayer;
60    private TextView mTextLine1;
61    private TextView mTextLine2;
62    private TextView mLoadingText;
63    private SeekBar mSeekBar;
64    private Handler mProgressRefresher;
65    private boolean mSeeking = false;
66    private int mDuration;
67    private Uri mUri;
68    private long mMediaId = -1;
69    private static final int OPEN_IN_MUSIC = 1;
70    private AudioManager mAudioManager;
71    private boolean mPausedByTransientLossOfFocus;
72
73    @Override
74    public void onCreate(Bundle icicle) {
75        super.onCreate(icicle);
76
77        Intent intent = getIntent();
78        if (intent == null) {
79            finish();
80            return;
81        }
82        mUri = intent.getData();
83        if (mUri == null) {
84            finish();
85            return;
86        }
87        String scheme = mUri.getScheme();
88
89        setVolumeControlStream(AudioManager.STREAM_MUSIC);
90        requestWindowFeature(Window.FEATURE_NO_TITLE);
91        setContentView(R.layout.audiopreview);
92
93        mTextLine1 = (TextView) findViewById(R.id.line1);
94        mTextLine2 = (TextView) findViewById(R.id.line2);
95        mLoadingText = (TextView) findViewById(R.id.loading);
96        if (scheme.equals("http")) {
97            String msg = getString(R.string.streamloadingtext, mUri.getHost());
98            mLoadingText.setText(msg);
99        } else {
100            mLoadingText.setVisibility(View.GONE);
101        }
102        mSeekBar = (SeekBar) findViewById(R.id.progress);
103        mProgressRefresher = new Handler();
104        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
105
106        PreviewPlayer player = (PreviewPlayer) getLastNonConfigurationInstance();
107        if (player == null) {
108            mPlayer = new PreviewPlayer();
109            mPlayer.setActivity(this);
110            try {
111                mPlayer.setDataSourceAndPrepare(mUri);
112            } catch (Exception ex) {
113                // catch generic Exception, since we may be called with a media
114                // content URI, another content provider's URI, a file URI,
115                // an http URI, and there are different exceptions associated
116                // with failure to open each of those.
117                Log.d(TAG, "Failed to open file: " + ex);
118                Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show();
119                finish();
120                return;
121            }
122        } else {
123            mPlayer = player;
124            mPlayer.setActivity(this);
125            if (mPlayer.isPrepared()) {
126                showPostPrepareUI();
127            }
128        }
129
130        AsyncQueryHandler mAsyncQueryHandler = new AsyncQueryHandler(getContentResolver()) {
131            @Override
132            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
133                if (cursor != null && cursor.moveToFirst()) {
134
135                    int titleIdx = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
136                    int artistIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
137                    int idIdx = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
138                    int displaynameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
139
140                    if (idIdx >=0) {
141                        mMediaId = cursor.getLong(idIdx);
142                    }
143
144                    if (titleIdx >= 0) {
145                        String title = cursor.getString(titleIdx);
146                        mTextLine1.setText(title);
147                        if (artistIdx >= 0) {
148                            String artist = cursor.getString(artistIdx);
149                            mTextLine2.setText(artist);
150                        }
151                    } else if (displaynameIdx >= 0) {
152                        String name = cursor.getString(displaynameIdx);
153                        mTextLine1.setText(name);
154                    } else {
155                        // Couldn't find anything to display, what to do now?
156                        Log.w(TAG, "Cursor had no names for us");
157                    }
158                } else {
159                    Log.w(TAG, "empty cursor");
160                }
161
162                if (cursor != null) {
163                    cursor.close();
164                }
165                setNames();
166            }
167        };
168
169        if (scheme.equals(ContentResolver.SCHEME_CONTENT)) {
170            if (mUri.getAuthority() == MediaStore.AUTHORITY) {
171                // try to get title and artist from the media content provider
172                mAsyncQueryHandler.startQuery(0, null, mUri, new String [] {
173                        MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST},
174                        null, null, null);
175            } else {
176                // Try to get the display name from another content provider.
177                // Don't specifically ask for the display name though, since the
178                // provider might not actually support that column.
179                mAsyncQueryHandler.startQuery(0, null, mUri, null, null, null, null);
180            }
181        } else if (scheme.equals("file")) {
182            // check if this file is in the media database (clicking on a download
183            // in the download manager might follow this path
184            String path = mUri.getPath();
185            mAsyncQueryHandler.startQuery(0, null,  MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
186                    new String [] {MediaStore.Audio.Media._ID,
187                        MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST},
188                    MediaStore.Audio.Media.DATA + "=?", new String [] {path}, null);
189        } else {
190            // We can't get metadata from the file/stream itself yet, because
191            // that API is hidden, so instead we display the URI being played
192            if (mPlayer.isPrepared()) {
193                setNames();
194            }
195        }
196    }
197
198    @Override
199    public Object onRetainNonConfigurationInstance() {
200        PreviewPlayer player = mPlayer;
201        mPlayer = null;
202        return player;
203    }
204
205    @Override
206    public void onDestroy() {
207        stopPlayback();
208        super.onDestroy();
209    }
210
211    private void stopPlayback() {
212        if (mProgressRefresher != null) {
213            mProgressRefresher.removeCallbacksAndMessages(null);
214        }
215        if (mPlayer != null) {
216            mPlayer.release();
217            mPlayer = null;
218            mAudioManager.abandonAudioFocus(mAudioFocusListener);
219        }
220    }
221
222    @Override
223    public void onUserLeaveHint() {
224        stopPlayback();
225        finish();
226        super.onUserLeaveHint();
227    }
228
229    public void onPrepared(MediaPlayer mp) {
230        if (isFinishing()) return;
231        mPlayer = (PreviewPlayer) mp;
232        setNames();
233        mPlayer.start();
234        showPostPrepareUI();
235    }
236
237    private void showPostPrepareUI() {
238        ProgressBar pb = (ProgressBar) findViewById(R.id.spinner);
239        pb.setVisibility(View.GONE);
240        mDuration = mPlayer.getDuration();
241        if (mDuration != 0) {
242            mSeekBar.setMax(mDuration);
243            mSeekBar.setVisibility(View.VISIBLE);
244        }
245        mSeekBar.setOnSeekBarChangeListener(mSeekListener);
246        mLoadingText.setVisibility(View.GONE);
247        View v = findViewById(R.id.titleandbuttons);
248        v.setVisibility(View.VISIBLE);
249        mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
250                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
251        mProgressRefresher.postDelayed(new ProgressRefresher(), 200);
252        updatePlayPause();
253    }
254
255    private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
256        public void onAudioFocusChange(int focusChange) {
257            if (mPlayer == null) {
258                // this activity has handed its MediaPlayer off to the next activity
259                // (e.g. portrait/landscape switch) and should abandon its focus
260                mAudioManager.abandonAudioFocus(this);
261                return;
262            }
263            switch (focusChange) {
264                case AudioManager.AUDIOFOCUS_LOSS:
265                    mPausedByTransientLossOfFocus = false;
266                    mPlayer.pause();
267                    break;
268                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
269                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
270                    if (mPlayer.isPlaying()) {
271                        mPausedByTransientLossOfFocus = true;
272                        mPlayer.pause();
273                    }
274                    break;
275                case AudioManager.AUDIOFOCUS_GAIN:
276                    if (mPausedByTransientLossOfFocus) {
277                        mPausedByTransientLossOfFocus = false;
278                        start();
279                    }
280                    break;
281            }
282            updatePlayPause();
283        }
284    };
285
286    private void start() {
287        mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
288                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
289        mPlayer.start();
290        mProgressRefresher.postDelayed(new ProgressRefresher(), 200);
291    }
292
293    public void setNames() {
294        if (TextUtils.isEmpty(mTextLine1.getText())) {
295            mTextLine1.setText(mUri.getLastPathSegment());
296        }
297        if (TextUtils.isEmpty(mTextLine2.getText())) {
298            mTextLine2.setVisibility(View.GONE);
299        } else {
300            mTextLine2.setVisibility(View.VISIBLE);
301        }
302    }
303
304    class ProgressRefresher implements Runnable {
305
306        public void run() {
307            if (mPlayer != null && !mSeeking && mDuration != 0) {
308                int progress = mPlayer.getCurrentPosition() / mDuration;
309                mSeekBar.setProgress(mPlayer.getCurrentPosition());
310            }
311            mProgressRefresher.removeCallbacksAndMessages(null);
312            mProgressRefresher.postDelayed(new ProgressRefresher(), 200);
313        }
314    }
315
316    private void updatePlayPause() {
317        ImageButton b = (ImageButton) findViewById(R.id.playpause);
318        if (b != null) {
319            if (mPlayer.isPlaying()) {
320                b.setImageResource(R.drawable.btn_playback_ic_pause_small);
321            } else {
322                b.setImageResource(R.drawable.btn_playback_ic_play_small);
323                mProgressRefresher.removeCallbacksAndMessages(null);
324            }
325        }
326    }
327
328    private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
329        public void onStartTrackingTouch(SeekBar bar) {
330            mSeeking = true;
331        }
332        public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
333            if (!fromuser) {
334                return;
335            }
336            // Protection for case of simultaneously tapping on seek bar and exit
337            if (mPlayer == null) {
338                return;
339            }
340            mPlayer.seekTo(progress);
341        }
342        public void onStopTrackingTouch(SeekBar bar) {
343            mSeeking = false;
344        }
345    };
346
347    public boolean onError(MediaPlayer mp, int what, int extra) {
348        Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show();
349        finish();
350        return true;
351    }
352
353    public void onCompletion(MediaPlayer mp) {
354        mSeekBar.setProgress(mDuration);
355        updatePlayPause();
356    }
357
358    public void playPauseClicked(View v) {
359        // Protection for case of simultaneously tapping on play/pause and exit
360        if (mPlayer == null) {
361            return;
362        }
363        if (mPlayer.isPlaying()) {
364            mPlayer.pause();
365        } else {
366            start();
367        }
368        updatePlayPause();
369    }
370
371    @Override
372    public boolean onCreateOptionsMenu(Menu menu) {
373        super.onCreateOptionsMenu(menu);
374        // TODO: if mMediaId != -1, then the playing file has an entry in the media
375        // database, and we could open it in the full music app instead.
376        // Ideally, we would hand off the currently running mediaplayer
377        // to the music UI, which can probably be done via a public static
378        menu.add(0, OPEN_IN_MUSIC, 0, "open in music");
379        return true;
380    }
381
382    @Override
383    public boolean onPrepareOptionsMenu(Menu menu) {
384        MenuItem item = menu.findItem(OPEN_IN_MUSIC);
385        if (mMediaId >= 0) {
386            item.setVisible(true);
387            return true;
388        }
389        item.setVisible(false);
390        return false;
391    }
392
393    @Override
394    public boolean onKeyDown(int keyCode, KeyEvent event) {
395        switch (keyCode) {
396            case KeyEvent.KEYCODE_HEADSETHOOK:
397            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
398                if (mPlayer.isPlaying()) {
399                    mPlayer.pause();
400                } else {
401                    start();
402                }
403                updatePlayPause();
404                return true;
405            case KeyEvent.KEYCODE_MEDIA_PLAY:
406                start();
407                updatePlayPause();
408                return true;
409            case KeyEvent.KEYCODE_MEDIA_PAUSE:
410                if (mPlayer.isPlaying()) {
411                    mPlayer.pause();
412                }
413                updatePlayPause();
414                return true;
415            case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
416            case KeyEvent.KEYCODE_MEDIA_NEXT:
417            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
418            case KeyEvent.KEYCODE_MEDIA_REWIND:
419                return true;
420            case KeyEvent.KEYCODE_MEDIA_STOP:
421            case KeyEvent.KEYCODE_BACK:
422                stopPlayback();
423                finish();
424                return true;
425        }
426        return super.onKeyDown(keyCode, event);
427    }
428
429    /*
430     * Wrapper class to help with handing off the MediaPlayer to the next instance
431     * of the activity in case of orientation change, without losing any state.
432     */
433    private static class PreviewPlayer extends MediaPlayer implements OnPreparedListener {
434        AudioPreview mActivity;
435        boolean mIsPrepared = false;
436
437        public void setActivity(AudioPreview activity) {
438            mActivity = activity;
439            setOnPreparedListener(this);
440            setOnErrorListener(mActivity);
441            setOnCompletionListener(mActivity);
442        }
443
444        public void setDataSourceAndPrepare(Uri uri) throws IllegalArgumentException,
445                        SecurityException, IllegalStateException, IOException {
446            setDataSource(mActivity,uri);
447            prepareAsync();
448        }
449
450        /* (non-Javadoc)
451         * @see android.media.MediaPlayer.OnPreparedListener#onPrepared(android.media.MediaPlayer)
452         */
453        @Override
454        public void onPrepared(MediaPlayer mp) {
455            mIsPrepared = true;
456            mActivity.onPrepared(mp);
457        }
458
459        boolean isPrepared() {
460            return mIsPrepared;
461        }
462    }
463
464}
465