2 * Copyright (C) 2015 The CyanogenMod Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.cyanogenmod.eleven.ui.activities.preview;
19 import android.app.Activity;
20 import android.content.AsyncQueryHandler;
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.pm.ActivityInfo;
26 import android.database.Cursor;
27 import android.graphics.Rect;
28 import android.media.AudioManager;
29 import android.media.AudioManager.OnAudioFocusChangeListener;
30 import android.media.MediaPlayer;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.os.Message;
35 import android.provider.MediaStore.Audio.Media;
36 import android.text.TextUtils;
37 import android.util.Log;
38 import android.view.KeyEvent;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.View.OnClickListener;
42 import android.view.View.OnTouchListener;
43 import android.view.Window;
44 import android.widget.ImageButton;
45 import android.widget.ProgressBar;
46 import android.widget.SeekBar;
47 import android.widget.SeekBar.OnSeekBarChangeListener;
48 import android.widget.TextView;
49 import android.widget.Toast;
50 import com.cyanogenmod.eleven.R;
51 import com.cyanogenmod.eleven.ui.activities.preview.util.Logger;
53 import java.io.IOException;
54 import java.lang.ref.WeakReference;
59 * Preview plays external audio files in a dialog over the application
62 * @see {@link Activity}
63 * @see {@link android.media.MediaPlayer.OnCompletionListener}
64 * @see {@link android.media.MediaPlayer.OnErrorListener}
65 * @see {@link android.media.MediaPlayer.OnPreparedListener}
66 * @see {@link OnClickListener}
67 * @see {@link OnAudioFocusChangeListener}
68 * @see {@link OnSeekBarChangeListener}
70 public class AudioPreviewActivity extends Activity implements MediaPlayer.OnCompletionListener,
71 MediaPlayer.OnErrorListener, MediaPlayer.OnPreparedListener, OnClickListener,
72 OnAudioFocusChangeListener, OnSeekBarChangeListener, OnTouchListener {
75 private static final String TAG = AudioPreviewActivity.class.getSimpleName();
76 private static final int PROGRESS_DELAY_INTERVAL = 250;
77 private static final String SCHEME_CONTENT = "content";
78 private static final String SCHEME_FILE = "file";
79 private static final String SCHEME_HTTP = "http";
80 private static final String AUTHORITY_MEDIA = "media";
81 private static final int CONTENT_QUERY_TOKEN = 1000;
82 private static final int CONTENT_BAD_QUERY_TOKEN = CONTENT_QUERY_TOKEN + 1;
83 private static final String[] MEDIA_PROJECTION = new String[] {
89 private boolean mIsSeeking = false;
90 private boolean mWasPlaying = false;
93 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
94 if (mPreviewPlayer != null && mIsSeeking) {
95 mPreviewPlayer.seekTo(progress);
100 public void onStartTrackingTouch(SeekBar seekBar) {
102 if (mCurrentState == State.PLAYING) {
104 pausePlayback(false);
109 public void onStopTrackingTouch(SeekBar seekBar) {
126 * Handle some ui events
129 * @see {@link Handler}
131 private class UiHandler extends Handler {
133 public static final int MSG_UPDATE_PROGRESS = 1000;
136 public void handleMessage(Message msg) {
138 case MSG_UPDATE_PROGRESS:
139 updateProgressForPlayer();
142 super.handleMessage(msg);
149 private final BroadcastReceiver mAudioNoisyReceiver = new BroadcastReceiver() {
151 public void onReceive(Context context, Intent intent) {
152 // [NOTE][MSB]: Handle any audio output changes
153 if (intent != null) {
154 if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
160 private UiHandler mHandler = new UiHandler();
161 private static AsyncQueryHandler sAsyncQueryHandler;
162 private AudioManager mAudioManager;
163 private PreviewPlayer mPreviewPlayer;
164 private PreviewSong mPreviewSong = new PreviewSong();
165 private int mDuration = 0;
166 private int mLastOrientationWhileBuffering;
169 private TextView mTitleTextView;
170 private TextView mArtistTextView;
171 private SeekBar mSeekBar;
172 private ProgressBar mProgressBar;
173 private ImageButton mPlayPauseBtn;
174 private View mContainerView;
177 private boolean mIsReceiverRegistered = false;
178 private State mCurrentState = State.INIT;
181 public void onCreate(Bundle savedInstanceState) {
182 overridePendingTransition(0, 0);
183 super.onCreate(savedInstanceState);
184 mLastOrientationWhileBuffering = getRequestedOrientation();
185 Logger.logd(TAG, "onCreate(" + savedInstanceState + ")");
186 Intent intent = getIntent();
187 if (intent == null) {
188 Logger.loge(TAG, "No intent");
192 Uri uri = intent.getData();
194 Logger.loge(TAG, "No uri data");
198 Logger.logd(TAG, "URI: " + uri);
199 mPreviewSong.URI = uri;
200 PreviewPlayer localPlayer = (PreviewPlayer) getLastNonConfigurationInstance();
201 if (localPlayer == null) {
202 mPreviewPlayer = new PreviewPlayer();
203 mPreviewPlayer.setCallbackActivity(this);
205 mPreviewPlayer.setDataSourceAndPrepare(mPreviewSong.URI);
206 } catch (IOException e) {
207 Logger.loge(TAG, e.getMessage());
208 onError(mPreviewPlayer, MediaPlayer.MEDIA_ERROR_IO, 0);
212 mPreviewPlayer = localPlayer;
213 mPreviewPlayer.setCallbackActivity(this);
215 mAudioManager = ((AudioManager) getSystemService(Context.AUDIO_SERVICE));
216 sAsyncQueryHandler = new AsyncQueryHandler(getContentResolver()) {
218 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
219 AudioPreviewActivity.this.onQueryComplete(token, cookie, cursor);
222 initializeInterface();
223 registerNoisyAudioReceiver();
224 if (savedInstanceState == null) {
227 mPreviewSong.TITLE = savedInstanceState.getString(Media.TITLE);
228 mPreviewSong.ARTIST = savedInstanceState.getString(Media.ARTIST);
231 if (localPlayer != null) {
232 sendStateChange(State.PREPARED);
233 if (localPlayer.isPlaying()) {
234 startProgressUpdates();
235 sendStateChange(State.PLAYING);
241 public void onSaveInstanceState(Bundle outState) {
242 if (mIsReceiverRegistered) {
243 unregisterReceiver(mAudioNoisyReceiver);
244 mIsReceiverRegistered = false;
246 outState.putString(Media.TITLE, mPreviewSong.TITLE);
247 outState.putString(Media.ARTIST, mPreviewSong.ARTIST);
248 super.onSaveInstanceState(outState);
252 public Object onRetainNonConfigurationInstance() {
253 mPreviewPlayer.clearCallbackActivity();
254 PreviewPlayer localPlayer = mPreviewPlayer;
255 mPreviewPlayer = null;
260 public void onPause() {
261 overridePendingTransition(0, 0);
266 public void onDestroy() {
267 if (mIsReceiverRegistered) {
268 unregisterReceiver(mAudioNoisyReceiver);
269 mIsReceiverRegistered = false;
271 stopPlaybackAndTeardown();
275 private void sendStateChange(State newState) {
276 mCurrentState = newState;
277 handleStateChangeForUi();
280 private void handleStateChangeForUi() {
281 switch (mCurrentState) {
283 Logger.logd(TAG, "INIT");
286 Logger.logd(TAG, "PREPARED");
287 if (mPreviewPlayer != null) {
288 mDuration = mPreviewPlayer.getDuration();
290 if (mDuration > 0 && mSeekBar != null) {
291 mSeekBar.setMax(mDuration);
292 mSeekBar.setEnabled(true);
293 mSeekBar.setVisibility(View.VISIBLE);
295 if (mProgressBar != null) {
296 mProgressBar.setVisibility(View.INVISIBLE);
297 setRequestedOrientation(mLastOrientationWhileBuffering);
299 if (mPlayPauseBtn != null) {
300 mPlayPauseBtn.setImageResource(R.drawable.btn_playback_play);
301 mPlayPauseBtn.setEnabled(true);
302 mPlayPauseBtn.setOnClickListener(this);
306 Logger.logd(TAG, "PLAYING");
307 if (mPlayPauseBtn != null) {
308 mPlayPauseBtn.setImageResource(R.drawable.btn_playback_pause);
309 mPlayPauseBtn.setEnabled(true);
313 Logger.logd(TAG, "PAUSED");
314 if (mPlayPauseBtn != null) {
315 mPlayPauseBtn.setImageResource(R.drawable.btn_playback_play);
316 mPlayPauseBtn.setEnabled(true);
323 private void onQueryComplete(int token, Object cookie, Cursor cursor) {
325 String artist = null;
326 if (cursor == null || cursor.getCount() < 1) {
327 Logger.loge(TAG, "Null or empty cursor!");
330 boolean moved = cursor.moveToFirst();
332 Logger.loge(TAG, "Failed to read cursor!");
337 case CONTENT_QUERY_TOKEN:
338 index = cursor.getColumnIndex(Media.TITLE);
340 title = cursor.getString(index);
342 index = cursor.getColumnIndex(Media.ARTIST);
344 artist = cursor.getString(index);
347 case CONTENT_BAD_QUERY_TOKEN:
348 index = cursor.getColumnIndex(Media.DISPLAY_NAME);
350 title = cursor.getString(index);
359 // Well if we didn't get the name lets fallback to something else
360 if (TextUtils.isEmpty(title)) {
361 title = getNameFromPath();
364 mPreviewSong.TITLE = title;
365 mPreviewSong.ARTIST = artist;
370 private String getNameFromPath() {
371 String path = "Unknown"; // [TODO][MSB]: Localize
372 if (mPreviewSong != null) {
373 if (mPreviewSong.URI != null) {
374 path = mPreviewSong.URI.getLastPathSegment();
380 private void setNames() {
382 mTitleTextView.setText(mPreviewSong.TITLE);
383 mArtistTextView.setText(mPreviewSong.ARTIST);
386 private void initializeInterface() {
387 setVolumeControlStream(AudioManager.STREAM_MUSIC);
388 requestWindowFeature(Window.FEATURE_NO_TITLE);
389 setContentView(R.layout.activity_audio_preview);
390 mContainerView = findViewById(R.id.grp_container_view);
391 // Make it so if the user touches the background overlay we exit
392 View v = findViewById(R.id.grp_transparent_wrapper);
393 v.setOnTouchListener(this);
394 mTitleTextView = (TextView) findViewById(R.id.tv_title);
395 mArtistTextView = (TextView) findViewById(R.id.tv_artist);
396 mSeekBar = (SeekBar) findViewById(R.id.sb_progress);
397 mSeekBar.setOnSeekBarChangeListener(this);
398 mProgressBar = (ProgressBar) findViewById(R.id.pb_loader);
399 mPlayPauseBtn = (ImageButton) findViewById(R.id.ib_playpause);
402 private void processUri() {
403 String scheme = mPreviewSong.URI.getScheme();
404 Logger.logd(TAG, "Uri Scheme: " + scheme);
405 if (SCHEME_CONTENT.equalsIgnoreCase(scheme)) {
406 handleContentScheme();
407 } else if (SCHEME_FILE.equalsIgnoreCase(scheme)) {
409 } else if (SCHEME_HTTP.equalsIgnoreCase(scheme)) {
414 private void startProgressUpdates() {
415 if (mHandler != null) {
416 mHandler.removeMessages(UiHandler.MSG_UPDATE_PROGRESS);
417 Message msg = mHandler.obtainMessage(UiHandler.MSG_UPDATE_PROGRESS);
418 mHandler.sendMessage(msg);
422 private void updateProgressForPlayer() {
423 if (mSeekBar != null && mPreviewPlayer != null) {
424 if (mPreviewPlayer.isPrepared()) {
425 mSeekBar.setProgress(mPreviewPlayer.getCurrentPosition());
428 if (mHandler != null) {
429 mHandler.removeMessages(UiHandler.MSG_UPDATE_PROGRESS);
430 Message msg = mHandler.obtainMessage(UiHandler.MSG_UPDATE_PROGRESS);
431 mHandler.sendMessageDelayed(msg, PROGRESS_DELAY_INTERVAL);
435 private void handleContentScheme() {
436 String authority = mPreviewSong.URI.getAuthority();
437 if (!AUTHORITY_MEDIA.equalsIgnoreCase(authority)) {
438 Logger.logd(TAG, "Bad authority!");
440 .startQuery(CONTENT_BAD_QUERY_TOKEN, null, mPreviewSong.URI, null, null, null,
444 .startQuery(CONTENT_QUERY_TOKEN, null, mPreviewSong.URI, MEDIA_PROJECTION, null,
449 private void handleFileScheme() {
450 String path = mPreviewSong.URI.getPath();
451 sAsyncQueryHandler.startQuery(CONTENT_QUERY_TOKEN, null, Media.EXTERNAL_CONTENT_URI,
452 MEDIA_PROJECTION, "_data=?", new String[] { path }, null);
455 private void handleHttpScheme() {
456 if (mProgressBar != null) {
457 mProgressBar.setVisibility(View.VISIBLE);
458 mLastOrientationWhileBuffering = getRequestedOrientation();
459 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
461 String path = getNameFromPath();
462 mPreviewSong.TITLE = path;
466 private void registerNoisyAudioReceiver() {
467 IntentFilter localIntentFilter = new IntentFilter();
468 localIntentFilter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
469 registerReceiver(this.mAudioNoisyReceiver, localIntentFilter);
470 mIsReceiverRegistered = true;
474 public void onCompletion(MediaPlayer mp) {
475 mHandler.removeMessages(UiHandler.MSG_UPDATE_PROGRESS);
476 if (mSeekBar != null && mPreviewPlayer != null) {
477 mSeekBar.setProgress(mPreviewPlayer.getCurrentPosition());
479 sendStateChange(State.PREPARED);
483 public boolean onError(MediaPlayer mp, int what, int extra) {
485 case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
486 Toast.makeText(this, "Server has died!", Toast.LENGTH_SHORT).show();
488 case MediaPlayer.MEDIA_ERROR_IO:
489 Toast.makeText(this, "I/O error!", Toast.LENGTH_SHORT).show();
491 case MediaPlayer.MEDIA_ERROR_MALFORMED:
492 Toast.makeText(this, "Malformed media!", Toast.LENGTH_SHORT).show();
494 case MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK:
495 Toast.makeText(this, "Not valid for progressive playback!", Toast.LENGTH_SHORT)
498 case MediaPlayer.MEDIA_ERROR_TIMED_OUT:
499 Toast.makeText(this, "Media server timed out!", Toast.LENGTH_SHORT).show();
501 case MediaPlayer.MEDIA_ERROR_UNSUPPORTED:
502 Toast.makeText(this, "Media is unsupported!", Toast.LENGTH_SHORT).show();
504 case MediaPlayer.MEDIA_ERROR_UNKNOWN:
506 Toast.makeText(this, "An unkown error has occurred: " + what, Toast.LENGTH_LONG)
510 stopPlaybackAndTeardown();
512 return true; // false causes flow to not call onCompletion
516 public void onPrepared(MediaPlayer mp) {
517 sendStateChange(State.PREPARED);
522 public boolean onTouch(View v, MotionEvent event) {
523 int x = (int) event.getX();
524 int y = (int) event.getY();
525 int containerX1 = (int) mContainerView.getX();
526 int containerY1 = (int) mContainerView.getY();
527 int containerX2 = (int) (mContainerView.getX() + mContainerView.getWidth());
528 int containerY2 = (int) (mContainerView.getY() + mContainerView.getHeight());
531 r.set(containerX1, containerY1, containerX2, containerY2);
532 if (!r.contains(x, y)) {
533 stopPlaybackAndTeardown();
541 public void onClick(View v) {
543 case R.id.ib_playpause:
544 if (mCurrentState == State.PREPARED || mCurrentState == State.PAUSED) {
550 case R.id.grp_transparent_wrapper:
551 stopPlaybackAndTeardown();
559 private boolean gainAudioFocus() {
560 if (mAudioManager == null) {
563 int r = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
564 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
565 return r == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
568 private void abandonAudioFocus() {
569 if (mAudioManager != null) {
570 mAudioManager.abandonAudioFocus(this);
574 private void startPlayback() {
575 if (mPreviewPlayer != null && !mPreviewPlayer.isPlaying()) {
576 if (mPreviewPlayer.isPrepared()) {
577 if (gainAudioFocus()) {
578 mPreviewPlayer.start();
579 sendStateChange(State.PLAYING);
580 startProgressUpdates();
582 Logger.loge(TAG, "Failed to gain audio focus!");
583 onError(mPreviewPlayer, MediaPlayer.MEDIA_ERROR_TIMED_OUT, 0);
586 Logger.loge(TAG, "Not prepared!");
589 Logger.logd(TAG, "No player or is not playing!");
593 private void stopPlaybackAndTeardown() {
594 if (mPreviewPlayer != null) {
595 if (mPreviewPlayer.isPlaying()) {
596 mPreviewPlayer.stop();
598 mPreviewPlayer.release();
599 mPreviewPlayer = null;
604 private void pausePlayback() {
608 private void pausePlayback(boolean updateUi) {
609 if (mPreviewPlayer != null && mPreviewPlayer.isPlaying()) {
610 mPreviewPlayer.pause();
612 sendStateChange(State.PAUSED);
614 mHandler.removeMessages(UiHandler.MSG_UPDATE_PROGRESS);
619 public void onAudioFocusChange(int focusChange) {
620 if (mPreviewPlayer == null) {
621 if (mAudioManager != null) {
622 mAudioManager.abandonAudioFocus(this);
625 Logger.logd(TAG, "Focus change: " + focusChange);
626 switch (focusChange) {
627 case AudioManager.AUDIOFOCUS_LOSS:
628 stopPlaybackAndTeardown();
631 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
634 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
635 mPreviewPlayer.setVolume(0.2f, 0.2f);
637 case AudioManager.AUDIOFOCUS_GAIN:
638 mPreviewPlayer.setVolume(1.0f, 1.0f);
641 case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
642 case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
648 public void onUserLeaveHint() {
649 stopPlaybackAndTeardown();
651 super.onUserLeaveHint();
655 public boolean onKeyDown(int keyCode, KeyEvent keyEvent) {
656 boolean result = true;
658 case KeyEvent.KEYCODE_HEADSETHOOK:
661 case KeyEvent.KEYCODE_MEDIA_NEXT:
662 case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
663 case KeyEvent.KEYCODE_MEDIA_REWIND:
664 case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
666 case KeyEvent.KEYCODE_MEDIA_PLAY:
669 case KeyEvent.KEYCODE_MEDIA_PAUSE:
672 case KeyEvent.KEYCODE_VOLUME_UP:
673 case KeyEvent.KEYCODE_VOLUME_DOWN:
674 case KeyEvent.KEYCODE_VOLUME_MUTE:
675 result = super.onKeyDown(keyCode, keyEvent);
678 result = super.onKeyDown(keyCode, keyEvent);
681 stopPlaybackAndTeardown();
688 * Media player specifically tweaked for use in this audio preview context
691 private static class PreviewPlayer extends MediaPlayer
692 implements MediaPlayer.OnPreparedListener {
695 private WeakReference<AudioPreviewActivity> mActivityReference; // weakref from static class
696 private boolean mIsPrepared = false;
698 /* package */ boolean isPrepared() {
702 /* package */ PreviewPlayer() {
703 setOnPreparedListener(this);
706 /* package */ void clearCallbackActivity() {
707 mActivityReference.clear();
708 mActivityReference = null;
709 setOnErrorListener(null);
710 setOnCompletionListener(null);
713 /* package */ void setCallbackActivity(AudioPreviewActivity activity)
714 throws IllegalArgumentException{
715 if (activity == null) {
716 throw new IllegalArgumentException("'activity' cannot be null!");
718 mActivityReference = new WeakReference<AudioPreviewActivity>(activity);
719 setOnErrorListener(activity);
720 setOnCompletionListener(activity);
723 /* package */ void setDataSourceAndPrepare(Uri uri)
724 throws IllegalArgumentException, IOException {
725 if (uri == null || uri.toString().length() < 1) {
726 throw new IllegalArgumentException("'uri' cannot be null or empty!");
728 AudioPreviewActivity activity = mActivityReference.get();
729 if (activity != null && !activity.isFinishing()) {
730 setDataSource(activity, uri);
736 public void onPrepared(MediaPlayer mp) {
738 if (mActivityReference != null) {
739 AudioPreviewActivity activity = mActivityReference.get();
740 if (activity != null && !activity.isFinishing()) {
741 activity.onPrepared(mp);