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