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