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 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 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 
134                     int titleIdx = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
135                     int artistIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
136                     int idIdx = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
137                     int displaynameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
138 
139                     if (idIdx >=0) {
140                         mMediaId = cursor.getLong(idIdx);
141                     }
142 
143                     if (titleIdx >= 0) {
144                         String title = cursor.getString(titleIdx);
145                         mTextLine1.setText(title);
146                         if (artistIdx >= 0) {
147                             String artist = cursor.getString(artistIdx);
148                             mTextLine2.setText(artist);
149                         }
150                     } else if (displaynameIdx >= 0) {
151                         String name = cursor.getString(displaynameIdx);
152                         mTextLine1.setText(name);
153                     } else {
154                         // Couldn't find anything to display, what to do now?
155                         Log.w(TAG, "Cursor had no names for us");
156                     }
157                 } else {
158                     Log.w(TAG, "empty cursor");
159                 }
160 
161                 if (cursor != null) {
162                     cursor.close();
163                 }
164                 setNames();
165             }
166         };
167 
168         if (scheme.equals(ContentResolver.SCHEME_CONTENT)) {
169             if (mUri.getAuthority() == MediaStore.AUTHORITY) {
170                 // try to get title and artist from the media content provider
171                 mAsyncQueryHandler.startQuery(0, null, mUri, new String [] {
172                         MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST},
173                         null, null, null);
174             } else {
175                 // Try to get the display name from another content provider.
176                 // Don't specifically ask for the display name though, since the
177                 // provider might not actually support that column.
178                 mAsyncQueryHandler.startQuery(0, null, mUri, null, null, null, null);
179             }
180         } else if (scheme.equals("file")) {
181             // check if this file is in the media database (clicking on a download
182             // in the download manager might follow this path
183             String path = mUri.getPath();
184             mAsyncQueryHandler.startQuery(0, null,  MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
185                     new String [] {MediaStore.Audio.Media._ID,
186                         MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST},
187                     MediaStore.Audio.Media.DATA + "=?", new String [] {path}, null);
188         } else {
189             // We can't get metadata from the file/stream itself yet, because
190             // that API is hidden, so instead we display the URI being played
191             if (mPlayer.isPrepared()) {
192                 setNames();
193             }
194         }
195     }
196 
197     @Override
onPause()198     public void onPause() {
199         super.onPause();
200         mUiPaused = true;
201         if (mProgressRefresher != null) {
202             mProgressRefresher.removeCallbacksAndMessages(null);
203         }
204     }
205 
206     @Override
onResume()207     public void onResume() {
208         super.onResume();
209         mUiPaused = false;
210         if (mPlayer.isPrepared()) {
211             showPostPrepareUI();
212         }
213     }
214 
215     @Override
onRetainNonConfigurationInstance()216     public Object onRetainNonConfigurationInstance() {
217         PreviewPlayer player = mPlayer;
218         mPlayer = null;
219         return player;
220     }
221 
222     @Override
onDestroy()223     public void onDestroy() {
224         stopPlayback();
225         super.onDestroy();
226     }
227 
stopPlayback()228     private void stopPlayback() {
229         if (mProgressRefresher != null) {
230             mProgressRefresher.removeCallbacksAndMessages(null);
231         }
232         if (mPlayer != null) {
233             mPlayer.release();
234             mPlayer = null;
235             mAudioManager.abandonAudioFocus(mAudioFocusListener);
236         }
237     }
238 
239     @Override
onUserLeaveHint()240     public void onUserLeaveHint() {
241         stopPlayback();
242         finish();
243         super.onUserLeaveHint();
244     }
245 
onPrepared(MediaPlayer mp)246     public void onPrepared(MediaPlayer mp) {
247         if (isFinishing()) return;
248         mPlayer = (PreviewPlayer) mp;
249         setNames();
250         mPlayer.start();
251         showPostPrepareUI();
252     }
253 
showPostPrepareUI()254     private void showPostPrepareUI() {
255         ProgressBar pb = (ProgressBar) findViewById(R.id.spinner);
256         pb.setVisibility(View.GONE);
257         mDuration = mPlayer.getDuration();
258         if (mDuration != 0) {
259             mSeekBar.setMax(mDuration);
260             mSeekBar.setVisibility(View.VISIBLE);
261             if (!mSeeking) {
262                 mSeekBar.setProgress(mPlayer.getCurrentPosition());
263             }
264         }
265         mSeekBar.setOnSeekBarChangeListener(mSeekListener);
266         mLoadingText.setVisibility(View.GONE);
267         View v = findViewById(R.id.titleandbuttons);
268         v.setVisibility(View.VISIBLE);
269         mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
270                 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
271         if (mProgressRefresher != null) {
272             mProgressRefresher.removeCallbacksAndMessages(null);
273             mProgressRefresher.postDelayed(new ProgressRefresher(), 200);
274         }
275         updatePlayPause();
276     }
277 
278     private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
279         public void onAudioFocusChange(int focusChange) {
280             if (mPlayer == null) {
281                 // this activity has handed its MediaPlayer off to the next activity
282                 // (e.g. portrait/landscape switch) and should abandon its focus
283                 mAudioManager.abandonAudioFocus(this);
284                 return;
285             }
286             switch (focusChange) {
287                 case AudioManager.AUDIOFOCUS_LOSS:
288                     mPausedByTransientLossOfFocus = false;
289                     mPlayer.pause();
290                     break;
291                 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
292                 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
293                     if (mPlayer.isPlaying()) {
294                         mPausedByTransientLossOfFocus = true;
295                         mPlayer.pause();
296                     }
297                     break;
298                 case AudioManager.AUDIOFOCUS_GAIN:
299                     if (mPausedByTransientLossOfFocus) {
300                         mPausedByTransientLossOfFocus = false;
301                         start();
302                     }
303                     break;
304             }
305             updatePlayPause();
306         }
307     };
308 
start()309     private void start() {
310         mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
311                 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
312         mPlayer.start();
313         mProgressRefresher.postDelayed(new ProgressRefresher(), 200);
314     }
315 
setNames()316     public void setNames() {
317         if (TextUtils.isEmpty(mTextLine1.getText())) {
318             mTextLine1.setText(mUri.getLastPathSegment());
319         }
320         if (TextUtils.isEmpty(mTextLine2.getText())) {
321             mTextLine2.setVisibility(View.GONE);
322         } else {
323             mTextLine2.setVisibility(View.VISIBLE);
324         }
325     }
326 
327     class ProgressRefresher implements Runnable {
328 
329         @Override
run()330         public void run() {
331             if (mPlayer != null && !mSeeking && mDuration != 0) {
332                 mSeekBar.setProgress(mPlayer.getCurrentPosition());
333             }
334             mProgressRefresher.removeCallbacksAndMessages(null);
335             if (!mUiPaused) {
336                 mProgressRefresher.postDelayed(new ProgressRefresher(), 200);
337             }
338         }
339     }
340 
updatePlayPause()341     private void updatePlayPause() {
342         ImageButton b = (ImageButton) findViewById(R.id.playpause);
343         if (b != null && mPlayer != null) {
344             if (mPlayer.isPlaying()) {
345                 b.setImageResource(R.drawable.btn_playback_ic_pause_small);
346             } else {
347                 b.setImageResource(R.drawable.btn_playback_ic_play_small);
348                 mProgressRefresher.removeCallbacksAndMessages(null);
349             }
350         }
351     }
352 
353     private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
354         public void onStartTrackingTouch(SeekBar bar) {
355             mSeeking = true;
356         }
357         public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
358             if (!fromuser) {
359                 return;
360             }
361             // Protection for case of simultaneously tapping on seek bar and exit
362             if (mPlayer == null) {
363                 return;
364             }
365             mPlayer.seekTo(progress);
366         }
367         public void onStopTrackingTouch(SeekBar bar) {
368             mSeeking = false;
369         }
370     };
371 
onError(MediaPlayer mp, int what, int extra)372     public boolean onError(MediaPlayer mp, int what, int extra) {
373         Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show();
374         finish();
375         return true;
376     }
377 
onCompletion(MediaPlayer mp)378     public void onCompletion(MediaPlayer mp) {
379         mSeekBar.setProgress(mDuration);
380         updatePlayPause();
381     }
382 
playPauseClicked(View v)383     public void playPauseClicked(View v) {
384         // Protection for case of simultaneously tapping on play/pause and exit
385         if (mPlayer == null) {
386             return;
387         }
388         if (mPlayer.isPlaying()) {
389             mPlayer.pause();
390         } else {
391             start();
392         }
393         updatePlayPause();
394     }
395 
396     @Override
onCreateOptionsMenu(Menu menu)397     public boolean onCreateOptionsMenu(Menu menu) {
398         super.onCreateOptionsMenu(menu);
399         // TODO: if mMediaId != -1, then the playing file has an entry in the media
400         // database, and we could open it in the full music app instead.
401         // Ideally, we would hand off the currently running mediaplayer
402         // to the music UI, which can probably be done via a public static
403         menu.add(0, OPEN_IN_MUSIC, 0, "open in music");
404         return true;
405     }
406 
407     @Override
onPrepareOptionsMenu(Menu menu)408     public boolean onPrepareOptionsMenu(Menu menu) {
409         MenuItem item = menu.findItem(OPEN_IN_MUSIC);
410         if (mMediaId >= 0) {
411             item.setVisible(true);
412             return true;
413         }
414         item.setVisible(false);
415         return false;
416     }
417 
418     @Override
onKeyDown(int keyCode, KeyEvent event)419     public boolean onKeyDown(int keyCode, KeyEvent event) {
420         switch (keyCode) {
421             case KeyEvent.KEYCODE_HEADSETHOOK:
422             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
423                 if (mPlayer.isPlaying()) {
424                     mPlayer.pause();
425                 } else {
426                     start();
427                 }
428                 updatePlayPause();
429                 return true;
430             case KeyEvent.KEYCODE_MEDIA_PLAY:
431                 start();
432                 updatePlayPause();
433                 return true;
434             case KeyEvent.KEYCODE_MEDIA_PAUSE:
435                 if (mPlayer.isPlaying()) {
436                     mPlayer.pause();
437                 }
438                 updatePlayPause();
439                 return true;
440             case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
441             case KeyEvent.KEYCODE_MEDIA_NEXT:
442             case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
443             case KeyEvent.KEYCODE_MEDIA_REWIND:
444                 return true;
445             case KeyEvent.KEYCODE_MEDIA_STOP:
446             case KeyEvent.KEYCODE_BACK:
447                 stopPlayback();
448                 finish();
449                 return true;
450         }
451         return super.onKeyDown(keyCode, event);
452     }
453 
454     /*
455      * Wrapper class to help with handing off the MediaPlayer to the next instance
456      * of the activity in case of orientation change, without losing any state.
457      */
458     private static class PreviewPlayer extends MediaPlayer implements OnPreparedListener {
459         AudioPreview mActivity;
460         boolean mIsPrepared = false;
461 
setActivity(AudioPreview activity)462         public void setActivity(AudioPreview activity) {
463             mActivity = activity;
464             setOnPreparedListener(this);
465             setOnErrorListener(mActivity);
466             setOnCompletionListener(mActivity);
467         }
468 
setDataSourceAndPrepare(Uri uri)469         public void setDataSourceAndPrepare(Uri uri) throws IllegalArgumentException,
470                         SecurityException, IllegalStateException, IOException {
471             setDataSource(mActivity,uri);
472             prepareAsync();
473         }
474 
475         /* (non-Javadoc)
476          * @see android.media.MediaPlayer.OnPreparedListener#onPrepared(android.media.MediaPlayer)
477          */
478         @Override
onPrepared(MediaPlayer mp)479         public void onPrepared(MediaPlayer mp) {
480             mIsPrepared = true;
481             mActivity.onPrepared(mp);
482         }
483 
isPrepared()484         boolean isPrepared() {
485             return mIsPrepared;
486         }
487     }
488 
489 }
490