/*
* Copyright (C) 2014 Google Inc. All Rights Reserved.
*
* 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.google.android.libraries.cast.companionlibrary.cast.player;
import static com.google.android.libraries.cast.companionlibrary.utils.LogUtils.LOGD;
import static com.google.android.libraries.cast.companionlibrary.utils.LogUtils.LOGE;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.libraries.cast.companionlibrary.R;
import com.google.android.libraries.cast.companionlibrary.cast.CastConfiguration;
import com.google.android.libraries.cast.companionlibrary.cast.VideoCastManager;
import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException;
import com.google.android.libraries.cast.companionlibrary.cast.exceptions
.TransientNetworkDisconnectionException;
import com.google.android.libraries.cast.companionlibrary.cast.tracks.ui.TracksChooserDialog;
import com.google.android.libraries.cast.companionlibrary.utils.LogUtils;
import com.google.android.libraries.cast.companionlibrary.utils.Utils;
import com.google.android.libraries.cast.companionlibrary.widgets.MiniController;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
/**
* This class provides an {@link android.app.Activity} that clients can easily add to their
* applications to provide an out-of-the-box remote player when a video is casting to a cast device.
* {@link com.google.android.libraries.cast.companionlibrary.cast.VideoCastManager} can manage the
* lifecycle and presentation of this activity.
* <p>
* This activity provides a number of controllers for managing the playback of the remote content:
* play/pause (or play/stop when a live stream is used) and seekbar (for non-live streams).
* <p>
* Clients who need to perform a pre-authorization process for playback can register a
* {@link MediaAuthListener} by calling
* {@link VideoCastManager#startVideoCastControllerActivity(android.content.Context, MediaAuthService)}
* In that case, this activity manages starting the {@link MediaAuthService} and will register a
* listener to handle the result.
*/
public class VideoCastControllerActivity extends AppCompatActivity implements
VideoCastController {
private static final String TAG = LogUtils
.makeLogTag(VideoCastControllerActivity.class);
public static final String TASK_TAG = "task";
public static final String DIALOG_TAG = "dialog";
private VideoCastManager mCastManager;
private View mPageView;
private ImageButton mPlayPause;
private TextView mLiveText;
private TextView mStart;
private TextView mEnd;
private SeekBar mSeekbar;
private TextView mLine2;
private ProgressBar mLoading;
private double mVolumeIncrement;
private View mControllers;
private Drawable mPauseDrawable;
private Drawable mPlayDrawable;
private Drawable mStopDrawable;
private OnVideoCastControllerListener mListener;
private int mStreamType;
private ImageButton mClosedCaptionIcon;
private ImageButton mSkipNext;
private ImageButton mSkipPrevious;
private View mPlaybackControls;
private Toolbar mToolbar;
private int mNextPreviousVisibilityPolicy
= CastConfiguration.NEXT_PREV_VISIBILITY_POLICY_DISABLED;
private boolean mImmersive;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.cast_activity);
loadAndSetupViews();
mCastManager = VideoCastManager.getInstance();
mImmersive = mCastManager.getCastConfiguration().isCastControllerImmersive();
mVolumeIncrement = mCastManager.getVolumeStep();
Bundle extras = getIntent().getExtras();
if (extras == null) {
finish();
return;
}
setUpActionBar();
FragmentManager fm = getSupportFragmentManager();
VideoCastControllerFragment videoCastControllerFragment
= (VideoCastControllerFragment) fm.findFragmentByTag(TASK_TAG);
// if fragment is null, it means this is the first time, so create it
if (videoCastControllerFragment == null) {
videoCastControllerFragment = VideoCastControllerFragment
.newInstance(extras);
fm.beginTransaction().add(videoCastControllerFragment, TASK_TAG).commit();
setOnVideoCastControllerChangedListener(videoCastControllerFragment);
} else {
setOnVideoCastControllerChangedListener(videoCastControllerFragment);
mListener.onConfigurationChanged();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.cast_player_menu, menu);
mCastManager.addMediaRouterButton(menu, R.id.media_route_menu_item);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
}
return true;
}
@Override
public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
return mCastManager.onDispatchVolumeKeyEvent(event, mVolumeIncrement) || super
.dispatchKeyEvent(event);
}
private void loadAndSetupViews() {
mPauseDrawable = getResources().getDrawable(R.drawable.ic_pause_circle_white_80dp);
mPlayDrawable = getResources().getDrawable(R.drawable.ic_play_circle_white_80dp);
mStopDrawable = getResources().getDrawable(R.drawable.ic_stop_circle_white_80dp);
mPageView = findViewById(R.id.pageview);
mPlayPause = (ImageButton) findViewById(R.id.play_pause_toggle);
mLiveText = (TextView) findViewById(R.id.live_text);
mStart = (TextView) findViewById(R.id.start_text);
mEnd = (TextView) findViewById(R.id.end_text);
mSeekbar = (SeekBar) findViewById(R.id.seekbar);
mLine2 = (TextView) findViewById(R.id.textview2);
mLoading = (ProgressBar) findViewById(R.id.progressbar1);
mControllers = findViewById(R.id.controllers);
mClosedCaptionIcon = (ImageButton) findViewById(R.id.cc);
mSkipNext = (ImageButton) findViewById(R.id.next);
mSkipPrevious = (ImageButton) findViewById(R.id.previous);
mPlaybackControls = findViewById(R.id.playback_controls);
((MiniController) findViewById(R.id.miniController1)).setCurrentVisibility(false);
setClosedCaptionState(CC_DISABLED);
mPlayPause.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
try {
mListener.onPlayPauseClicked(v);
} catch (TransientNetworkDisconnectionException e) {
LOGE(TAG, "Failed to toggle playback due to temporary network issue", e);
Utils.showToast(VideoCastControllerActivity.this,
R.string.ccl_failed_no_connection_trans);
} catch (NoConnectionException e) {
LOGE(TAG, "Failed to toggle playback due to network issues", e);
Utils.showToast(VideoCastControllerActivity.this,
R.string.ccl_failed_no_connection);
} catch (Exception e) {
LOGE(TAG, "Failed to toggle playback due to other issues", e);
Utils.showToast(VideoCastControllerActivity.this,
R.string.ccl_failed_perform_action);
}
}
});
mSeekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
try {
if (mListener != null) {
mListener.onStopTrackingTouch(seekBar);
}
} catch (Exception e) {
LOGE(TAG, "Failed to complete seek", e);
finish();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
try {
if (mListener != null) {
mListener.onStartTrackingTouch(seekBar);
}
} catch (Exception e) {
LOGE(TAG, "Failed to start seek", e);
finish();
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
mStart.setText(Utils.formatMillis(progress));
try {
if (mListener != null) {
mListener.onProgressChanged(seekBar, progress, fromUser);
}
} catch (Exception e) {
LOGE(TAG, "Failed to set the progress result", e);
}
}
});
mClosedCaptionIcon.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
try {
showTracksChooserDialog();
} catch (TransientNetworkDisconnectionException | NoConnectionException e) {
LOGE(TAG, "Failed to get the media", e);
}
}
});
mSkipNext.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
try {
mListener.onSkipNextClicked(v);
} catch (TransientNetworkDisconnectionException | NoConnectionException e) {
LOGE(TAG, "Failed to move to the next item in the queue", e);
}
}
});
mSkipPrevious.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
try {
mListener.onSkipPreviousClicked(v);
} catch (TransientNetworkDisconnectionException | NoConnectionException e) {
LOGE(TAG, "Failed to move to the previous item in the queue", e);
}
}
});
}
private void showTracksChooserDialog()
throws TransientNetworkDisconnectionException, NoConnectionException {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
Fragment prev = getSupportFragmentManager().findFragmentByTag(DIALOG_TAG);
if (prev != null) {
transaction.remove(prev);
}
transaction.addToBackStack(null);
// Create and show the dialog.
TracksChooserDialog dialogFragment = TracksChooserDialog
.newInstance(mCastManager.getRemoteMediaInformation());
dialogFragment.show(transaction, DIALOG_TAG);
}
private void setUpActionBar() {
mToolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
@Override
public void showLoading(boolean visible) {
mLoading.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
}
@Override
public void adjustControllersForLiveStream(boolean isLive) {
int visibility = isLive ? View.INVISIBLE : View.VISIBLE;
mLiveText.setVisibility(isLive ? View.VISIBLE : View.INVISIBLE);
mStart.setVisibility(visibility);
mEnd.setVisibility(visibility);
mSeekbar.setVisibility(visibility);
}
@Override
public void setClosedCaptionState(int status) {
switch (status) {
case CC_ENABLED:
mClosedCaptionIcon.setVisibility(View.VISIBLE);
mClosedCaptionIcon.setEnabled(true);
break;
case CC_DISABLED:
mClosedCaptionIcon.setVisibility(View.VISIBLE);
mClosedCaptionIcon.setEnabled(false);
break;
case CC_HIDDEN:
mClosedCaptionIcon.setVisibility(View.GONE);
break;
default:
LOGE(TAG, "setClosedCaptionState(): Invalid state requested: " + status);
}
}
@Override
public void onQueueItemsUpdated(int queueLength, int position) {
boolean prevAvailable = position > 0;
boolean nextAvailable = position < queueLength - 1;
switch(mNextPreviousVisibilityPolicy) {
case CastConfiguration.NEXT_PREV_VISIBILITY_POLICY_HIDDEN:
if (nextAvailable) {
mSkipNext.setVisibility(View.VISIBLE);
mSkipNext.setEnabled(true);
} else {
mSkipNext.setVisibility(View.INVISIBLE);
}
if (prevAvailable) {
mSkipPrevious.setVisibility(View.VISIBLE);
mSkipPrevious.setEnabled(true);
} else {
mSkipPrevious.setVisibility(View.INVISIBLE);
}
break;
case CastConfiguration.NEXT_PREV_VISIBILITY_POLICY_ALWAYS:
mSkipNext.setVisibility(View.VISIBLE);
mSkipNext.setEnabled(true);
mSkipPrevious.setVisibility(View.VISIBLE);
mSkipPrevious.setEnabled(true);
break;
case CastConfiguration.NEXT_PREV_VISIBILITY_POLICY_DISABLED:
if (nextAvailable) {
mSkipNext.setVisibility(View.VISIBLE);
mSkipNext.setEnabled(true);
} else {
mSkipNext.setVisibility(View.VISIBLE);
mSkipNext.setEnabled(false);
}
if (prevAvailable) {
mSkipPrevious.setVisibility(View.VISIBLE);
mSkipPrevious.setEnabled(true);
} else {
mSkipPrevious.setVisibility(View.VISIBLE);
mSkipPrevious.setEnabled(false);
}
break;
default:
LOGE(TAG, "onQueueItemsUpdated(): Invalid NextPreviousPolicy has been set");
}
}
@Override
public void setPlaybackStatus(int state) {
LOGD(TAG, "setPlaybackStatus(): state = " + state);
switch (state) {
case MediaStatus.PLAYER_STATE_PLAYING:
mLoading.setVisibility(View.INVISIBLE);
mPlaybackControls.setVisibility(View.VISIBLE);
if (mStreamType == MediaInfo.STREAM_TYPE_LIVE) {
mPlayPause.setImageDrawable(mStopDrawable);
} else {
mPlayPause.setImageDrawable(mPauseDrawable);
}
mLine2.setText(getString(R.string.ccl_casting_to_device,
mCastManager.getDeviceName()));
mControllers.setVisibility(View.VISIBLE);
break;
case MediaStatus.PLAYER_STATE_PAUSED:
mControllers.setVisibility(View.VISIBLE);
mLoading.setVisibility(View.INVISIBLE);
mPlaybackControls.setVisibility(View.VISIBLE);
mPlayPause.setImageDrawable(mPlayDrawable);
mLine2.setText(getString(R.string.ccl_casting_to_device,
mCastManager.getDeviceName()));
break;
case MediaStatus.PLAYER_STATE_IDLE:
if (mStreamType == MediaInfo.STREAM_TYPE_LIVE) {
mControllers.setVisibility(View.VISIBLE);
mLoading.setVisibility(View.INVISIBLE);
mPlaybackControls.setVisibility(View.VISIBLE);
mPlayPause.setImageDrawable(mPlayDrawable);
mLine2.setText(getString(R.string.ccl_casting_to_device,
mCastManager.getDeviceName()));
} else {
mPlaybackControls.setVisibility(View.INVISIBLE);
mLoading.setVisibility(View.VISIBLE);
mLine2.setText(getString(R.string.ccl_loading));
}
break;
case MediaStatus.PLAYER_STATE_BUFFERING:
mPlaybackControls.setVisibility(View.INVISIBLE);
mLoading.setVisibility(View.VISIBLE);
mLine2.setText(getString(R.string.ccl_loading));
break;
default:
}
}
@Override
public void updateSeekbar(int position, int duration) {
mSeekbar.setProgress(position);
mSeekbar.setMax(duration);
mStart.setText(Utils.formatMillis(position));
mEnd.setText(Utils.formatMillis(duration));
}
@SuppressWarnings("deprecation")
@Override
public void setImage(Bitmap bitmap) {
if (bitmap != null) {
if (mPageView instanceof ImageView) {
((ImageView) mPageView).setImageBitmap(bitmap);
} else {
mPageView.setBackgroundDrawable(new BitmapDrawable(getResources(), bitmap));
}
}
}
@Override
public void setTitle(String text) {
mToolbar.setTitle(text);
}
@Override
public void setSubTitle(String text) {
mLine2.setText(text);
}
@Override
public void setOnVideoCastControllerChangedListener(OnVideoCastControllerListener listener) {
if (listener != null) {
mListener = listener;
}
}
@Override
public void setStreamType(int streamType) {
this.mStreamType = streamType;
}
@Override
public void updateControllersStatus(boolean enabled) {
mControllers.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
if (enabled) {
adjustControllersForLiveStream(mStreamType == MediaInfo.STREAM_TYPE_LIVE);
}
}
@Override
public void closeActivity() {
finish();
}
@Override // from VideoCastController
public void setNextPreviousVisibilityPolicy(@CastConfiguration.PrevNextPolicy int policy) {
mNextPreviousVisibilityPolicy = policy;
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus && mImmersive) {
setImmersive();
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void setImmersive() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return;
}
int newUiOptions = getWindow().getDecorView().getSystemUiVisibility();
// Navigation bar hiding: Backwards compatible to ICS.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
newUiOptions ^= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
}
// Status bar hiding: Backwards compatible to Jellybean
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
newUiOptions ^= View.SYSTEM_UI_FLAG_FULLSCREEN;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
newUiOptions ^= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
}
getWindow().getDecorView().setSystemUiVisibility(newUiOptions);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
setImmersive(true);
}
}
}