/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.torrenttunes.android.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.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.torrenttunes.android.AlbumArtCache;
import com.torrenttunes.android.MusicService;
import com.torrenttunes.android.R;
import com.torrenttunes.android.utils.LogHelper;
import com.google.android.libraries.cast.companionlibrary.utils.Utils;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
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 = LogHelper.makeLogTag(FullScreenPlayerActivity.class);
private static final long PROGRESS_UPDATE_INTERNAL = 1000;
private static final long PROGRESS_UPDATE_INITIAL_INTERVAL = 100;
private ImageView mSkipPrev;
private ImageView mSkipNext;
private ImageView mPlayPause;
private TextView mStart;
private TextView mEnd;
private SeekBar mSeekbar;
private TextView mLine1;
private TextView mLine2;
private TextView mLine3;
private ProgressBar mLoading;
private View mControllers;
private Drawable mPauseDrawable;
private Drawable mPlayDrawable;
private 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) {
LogHelper.d(TAG, "onPlaybackstate changed", state);
updatePlaybackState(state);
}
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
if (metadata != null) {
updateMediaDescription(metadata.getDescription());
updateDuration(metadata);
}
}
};
private MediaBrowserCompat.ConnectionCallback mMediaBrowserConnectionCallback =
new MediaBrowserCompat.ConnectionCallback() {
@Override
public void onConnected() {
LogHelper.d(TAG, "onConnected");
MediaSessionCompat.Token token = mMediaBrowser.getSessionToken();
if (token == null) {
throw new IllegalArgumentException("No Session token");
}
connectToSession(token);
}
};
private MediaControllerCompat mMediaController;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_full_player);
initializeToolbar();
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle("");
mBackgroundImage = (ImageView) findViewById(R.id.background_image);
mPauseDrawable = ActivityCompat.getDrawable(this, R.drawable.ic_pause_white_48dp);
mPlayDrawable = ActivityCompat.getDrawable(this, R.drawable.ic_play_arrow_white_48dp);
mPlayPause = (ImageView) findViewById(R.id.imageView1);
mSkipNext = (ImageView) findViewById(R.id.next);
mSkipPrev = (ImageView) findViewById(R.id.prev);
mStart = (TextView) findViewById(R.id.startText);
mEnd = (TextView) findViewById(R.id.endText);
mSeekbar = (SeekBar) findViewById(R.id.seekBar1);
mLine1 = (TextView) findViewById(R.id.line1);
mLine2 = (TextView) findViewById(R.id.line2);
mLine3 = (TextView) findViewById(R.id.line3);
mLoading = (ProgressBar) findViewById(R.id.progressBar1);
mControllers = findViewById(R.id.controllers);
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();
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:
LogHelper.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), mMediaBrowserConnectionCallback, 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);
}
}
@Override
public void onDestroy() {
super.onDestroy();
stopSeekbarUpdate();
mExecutorService.shutdown();
}
private void fetchImageAsync(MediaDescriptionCompat description) {
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;
}
LogHelper.d(TAG, "updateMediaDescription called ");
mLine1.setText(description.getTitle());
mLine2.setText(description.getSubtitle());
fetchImageAsync(description);
}
private void updateDuration(MediaMetadataCompat metadata) {
if (metadata == null) {
return;
}
LogHelper.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;
String castName = mMediaController.getExtras().getString(MusicService.EXTRA_CONNECTED_CAST);
String line3Text = "";
if (castName != null) {
line3Text = 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:
LogHelper.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);
}
}