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