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);
}
}