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 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