package com.markzhai.lyrichere.ui; import android.content.ComponentName; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.browse.MediaBrowserCompat; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.view.View; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.TextView; import com.hannesdorfmann.mosby.mvp.MvpBasePresenter; import com.hannesdorfmann.mosby.mvp.MvpPresenter; import com.markzhai.lyrichere.AlbumArtCache; import com.markzhai.lyrichere.MusicService; import com.markzhai.lyrichere.R; import com.markzhai.lyrichere.utils.LogUtils; import com.markzhai.lyrichere.utils.Utils; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import butterknife.Bind; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; /** * A full screen player that shows the current playing music with a background image * depicting the album art. The activity also has controls to seek/pause/play the audio. */ public class FullScreenPlayerActivity extends ActionBarCastActivity { private static final String TAG = LogUtils.makeLogTag(FullScreenPlayerActivity.class); private static final long PROGRESS_UPDATE_INTERNAL = 1000; private static final long PROGRESS_UPDATE_INITIAL_INTERVAL = 100; @Bind(R.id.prev) ImageView mSkipPrev; @Bind(R.id.next) ImageView mSkipNext; @Bind(R.id.play_pause) ImageView mPlayPause; @Bind(R.id.startText) TextView mStart; @Bind(R.id.endText) TextView mEnd; @Bind(R.id.seekBar1) SeekBar mSeekBar; @Bind(R.id.line1) TextView mLine1; @Bind(R.id.line2) TextView mLine2; @Bind(R.id.line3) TextView mLine3; @Bind(R.id.progressBar1) ProgressBar mLoading; @Bind(R.id.controllers) View mControllers; private Drawable mPauseDrawable; private Drawable mPlayDrawable; @Bind(R.id.background_image) ImageView mBackgroundImage; private String mCurrentArtUrl; private Handler mHandler = new Handler(); private MediaBrowserCompat mMediaBrowser; private final Runnable mUpdateProgressTask = new Runnable() { @Override public void run() { updateProgress(); } }; private final ScheduledExecutorService mExecutorService = Executors.newSingleThreadScheduledExecutor(); private ScheduledFuture<?> mScheduleFuture; private PlaybackStateCompat mLastPlaybackState; private MediaControllerCompat.Callback mCallback = new MediaControllerCompat.Callback() { @Override public void onPlaybackStateChanged(PlaybackStateCompat state) { LogUtils.d(TAG, "onPlaybackStateChanged", state); updatePlaybackState(state); } @Override public void onMetadataChanged(MediaMetadataCompat metadata) { if (metadata != null) { updateMediaDescription(metadata.getDescription()); updateDuration(metadata); } } }; private final MediaBrowserCompat.ConnectionCallback mConnectionCallback = new MediaBrowserCompat.ConnectionCallback() { @Override public void onConnected() { LogUtils.d(TAG, "onConnected"); connectToSession(mMediaBrowser.getSessionToken()); } }; private MediaControllerCompat mMediaController; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_full_player); initializeToolbar(); if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setTitle(""); } mPauseDrawable = ActivityCompat.getDrawable(this, R.drawable.ic_pause_white_48dp); mPlayDrawable = ActivityCompat.getDrawable(this, R.drawable.ic_play_arrow_white_48dp); mSkipNext.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { MediaControllerCompat.TransportControls controls = mMediaController.getTransportControls(); controls.skipToNext(); } }); mSkipPrev.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { MediaControllerCompat.TransportControls controls = mMediaController.getTransportControls(); controls.skipToPrevious(); } }); mPlayPause.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { PlaybackStateCompat state = mMediaController.getPlaybackState(); if (state != null) { MediaControllerCompat.TransportControls controls = mMediaController.getTransportControls(); switch (state.getState()) { case PlaybackStateCompat.STATE_PLAYING: // fall through case PlaybackStateCompat.STATE_BUFFERING: controls.pause(); stopSeekbarUpdate(); break; case PlaybackStateCompat.STATE_PAUSED: case PlaybackStateCompat.STATE_STOPPED: controls.play(); scheduleSeekbarUpdate(); break; default: LogUtils.d(TAG, "onClick with state ", state.getState()); } } } }); mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { mStart.setText(Utils.formatMillis(progress)); } @Override public void onStartTrackingTouch(SeekBar seekBar) { stopSeekbarUpdate(); } @Override public void onStopTrackingTouch(SeekBar seekBar) { mMediaController.getTransportControls().seekTo(seekBar.getProgress()); scheduleSeekbarUpdate(); } }); // Only update from the intent if we are not recreating from a config change: if (savedInstanceState == null) { updateFromParams(getIntent()); } mMediaBrowser = new MediaBrowserCompat(this, new ComponentName(this, MusicService.class), mConnectionCallback, null); } private void connectToSession(MediaSessionCompat.Token token) { try { mMediaController = new MediaControllerCompat(FullScreenPlayerActivity.this, token); if (mMediaController.getMetadata() == null) { finish(); return; } mMediaController.registerCallback(mCallback); PlaybackStateCompat state = mMediaController.getPlaybackState(); updatePlaybackState(state); MediaMetadataCompat metadata = mMediaController.getMetadata(); if (metadata != null) { updateMediaDescription(metadata.getDescription()); updateDuration(metadata); } updateProgress(); if (state != null && (state.getState() == PlaybackStateCompat.STATE_PLAYING || state.getState() == PlaybackStateCompat.STATE_BUFFERING)) { scheduleSeekbarUpdate(); } } catch (RemoteException e) { e.printStackTrace(); } } private void updateFromParams(Intent intent) { if (intent != null) { MediaDescriptionCompat description = intent.getParcelableExtra( MusicPlayerActivity.EXTRA_CURRENT_MEDIA_DESCRIPTION); if (description != null) { updateMediaDescription(description); } } } private void scheduleSeekbarUpdate() { stopSeekbarUpdate(); if (!mExecutorService.isShutdown()) { mScheduleFuture = mExecutorService.scheduleAtFixedRate( new Runnable() { @Override public void run() { mHandler.post(mUpdateProgressTask); } }, PROGRESS_UPDATE_INITIAL_INTERVAL, PROGRESS_UPDATE_INTERNAL, TimeUnit.MILLISECONDS); } } private void stopSeekbarUpdate() { if (mScheduleFuture != null) { mScheduleFuture.cancel(false); } } @Override public void onStart() { super.onStart(); if (mMediaBrowser != null) { mMediaBrowser.connect(); } } @Override public void onStop() { super.onStop(); if (mMediaBrowser != null) { mMediaBrowser.disconnect(); } if (mMediaController != null) { mMediaController.unregisterCallback(mCallback); } } @NonNull @Override public MvpPresenter createPresenter() { return new MvpBasePresenter(); } @Override public void onDestroy() { super.onDestroy(); stopSeekbarUpdate(); mExecutorService.shutdown(); } private void fetchImageAsync(@NonNull MediaDescriptionCompat description) { if (description.getIconUri() == null) { return; } String artUrl = description.getIconUri().toString(); mCurrentArtUrl = artUrl; AlbumArtCache cache = AlbumArtCache.getInstance(); Bitmap art = cache.getBigImage(artUrl); if (art == null) { art = description.getIconBitmap(); } if (art != null && !art.isRecycled()) { // if we have the art cached or from the MediaDescription, use it: mBackgroundImage.setImageBitmap(art); } else { // otherwise, fetch a high res version and update: cache.fetch(artUrl, new AlbumArtCache.FetchListener() { @Override public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) { // sanity check, in case a new fetch request has been done while // the previous hasn't yet returned: if (!bitmap.isRecycled() && artUrl.equals(mCurrentArtUrl)) { mBackgroundImage.setImageBitmap(bitmap); } } }); } } private void updateMediaDescription(MediaDescriptionCompat description) { if (description == null) { return; } LogUtils.d(TAG, "updateMediaDescription called "); mLine1.setText(description.getTitle()); mLine2.setText(description.getSubtitle()); fetchImageAsync(description); } private void updateDuration(MediaMetadataCompat metadata) { if (metadata == null) { return; } LogUtils.d(TAG, "updateDuration called "); int duration = (int) metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION); mSeekBar.setMax(duration); mEnd.setText(Utils.formatMillis(duration)); } private void updatePlaybackState(PlaybackStateCompat state) { if (state == null) { return; } mLastPlaybackState = state; if (mMediaController != null && mMediaController.getExtras() != null) { String castName = mMediaController.getExtras().getString(MusicService.EXTRA_CONNECTED_CAST); String line3Text = castName == null ? "" : getResources() .getString(R.string.casting_to_device, castName); mLine3.setText(line3Text); } switch (state.getState()) { case PlaybackStateCompat.STATE_PLAYING: mLoading.setVisibility(INVISIBLE); mPlayPause.setVisibility(VISIBLE); mPlayPause.setImageDrawable(mPauseDrawable); mControllers.setVisibility(VISIBLE); scheduleSeekbarUpdate(); break; case PlaybackStateCompat.STATE_PAUSED: mControllers.setVisibility(VISIBLE); mLoading.setVisibility(INVISIBLE); mPlayPause.setVisibility(VISIBLE); mPlayPause.setImageDrawable(mPlayDrawable); stopSeekbarUpdate(); break; case PlaybackStateCompat.STATE_NONE: case PlaybackStateCompat.STATE_STOPPED: mLoading.setVisibility(INVISIBLE); mPlayPause.setVisibility(VISIBLE); mPlayPause.setImageDrawable(mPlayDrawable); stopSeekbarUpdate(); break; case PlaybackStateCompat.STATE_BUFFERING: mPlayPause.setVisibility(INVISIBLE); mLoading.setVisibility(VISIBLE); mLine3.setText(R.string.loading); stopSeekbarUpdate(); break; default: LogUtils.d(TAG, "Unhandled state ", state.getState()); } mSkipNext.setVisibility((state.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) == 0 ? INVISIBLE : VISIBLE); mSkipPrev.setVisibility((state.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) == 0 ? INVISIBLE : VISIBLE); } private void updateProgress() { if (mLastPlaybackState == null) { return; } long currentPosition = mLastPlaybackState.getPosition(); if (mLastPlaybackState.getState() != PlaybackStateCompat.STATE_PAUSED) { // Calculate the elapsed time between the last position update and now and unless // paused, we can assume (delta * speed) + current position is approximately the // latest position. This ensure that we do not repeatedly call the getPlaybackState() // on MediaController. long timeDelta = SystemClock.elapsedRealtime() - mLastPlaybackState.getLastPositionUpdateTime(); currentPosition += (int) timeDelta * mLastPlaybackState.getPlaybackSpeed(); } mSeekBar.setProgress((int) currentPosition); } }