/* * Copyright (C) 2015 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.widgets; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaStatus; import com.google.android.libraries.cast.companionlibrary.R; import com.google.android.libraries.cast.companionlibrary.cast.VideoCastManager; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.OnFailedListener; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException; import com.google.android.libraries.cast.companionlibrary.utils.FetchBitmapTask; import com.google.android.libraries.cast.companionlibrary.utils.Utils; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; /** * A compound component that provides a superset of functionalities required for the global access * requirement. This component provides an image for the album art, a play/pause button, and a * progressbar to show the current position. When an auto-play queue is playing and pre-loading is * set, then this component can show an additional view to inform the user of the upcoming item and * to allow immediate playback of the next item or to stop the auto-play. * * <p>Clients can add this * compound component to their layout xml and preferably set the {@code auto_setup} attribute to * {@code true} to have the CCL manage the visibility and behavior of this component. Alternatively, * clients can register this component with the instance of * {@link VideoCastManager} by using the following pattern:<br/> * * <pre> * mMiniController = (MiniController) findViewById(R.id.miniController); * mCastManager.addMiniController(mMiniController); * mMiniController.setOnMiniControllerChangedListener(mCastManager); * </pre> * * In this case, clients should remember to unregister the component themselves. * Then the {@link VideoCastManager} will manage the behavior, including its state and metadata and * interactions. Note that using the {@code auto_setup} attribute hand;les all of these * automatically. */ public class MiniController extends RelativeLayout implements IMiniController { public static final int UNDEFINED_STATUS_CODE = -1; private boolean mAutoSetup; private VideoCastManager mCastManager; private Handler mHandler; protected ImageView mIcon; protected TextView mTitle; protected TextView mSubTitle; protected ImageView mPlayPause; protected ProgressBar mLoading; private OnMiniControllerChangedListener mListener; private Uri mIconUri; private Drawable mPauseDrawable; private Drawable mPlayDrawable; private int mStreamType = MediaInfo.STREAM_TYPE_BUFFERED; private Drawable mStopDrawable; private FetchBitmapTask mFetchBitmapTask; private ProgressBar mProgressBar; private ImageView mUpcomingIcon; private TextView mUpcomingTitle; private View mUpcomingContainer; private View mUpcomingPlay; private View mUpcomingStop; private Uri mUpcomingIconUri; private FetchBitmapTask mFetchUpcomingBitmapTask; private View mMainContainer; private MediaQueueItem mUpcomingItem; public MiniController(Context context, AttributeSet attrs) { super(context, attrs); LayoutInflater inflater = LayoutInflater.from(context); inflater.inflate(R.layout.mini_controller, this); TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MiniController); mAutoSetup = a.getBoolean(R.styleable.MiniController_auto_setup, false); a.recycle(); mPauseDrawable = getResources().getDrawable(R.drawable.ic_mini_controller_pause); mPlayDrawable = getResources().getDrawable(R.drawable.ic_mini_controller_play); mStopDrawable = getResources().getDrawable(R.drawable.ic_mini_controller_stop); mHandler = new Handler(); if (!isInEditMode()) { mCastManager = VideoCastManager.getInstance(); } loadViews(); setUpCallbacks(); } @Override public void setVisibility(int visibility) { super.setVisibility(visibility); if (visibility == View.VISIBLE) { mProgressBar.setProgress(0); } } /** * Sets the listener that should be notified when a relevant event is fired from this * component. * Clients can register the {@link VideoCastManager} instance to be the default listener so it * can control the remote media playback. */ @Override public void setOnMiniControllerChangedListener(OnMiniControllerChangedListener listener) { if (listener != null) { this.mListener = listener; } } /** * Removes the listener that was registered by * {@link #setOnMiniControllerChangedListener(OnMiniControllerChangedListener)} */ public void removeOnMiniControllerChangedListener(OnMiniControllerChangedListener listener) { if ((listener != null) && (mListener == listener)) { mListener = null; } } @Override public void setStreamType(int streamType) { mStreamType = streamType; } @Override public void setProgress(final int progress, final int duration) { // for live streams, we do not attempt to update the progress bar if (mStreamType == MediaInfo.STREAM_TYPE_LIVE || mProgressBar == null) { return; } mHandler.post(new Runnable() { @Override public void run() { mProgressBar.setMax(duration); mProgressBar.setProgress(progress); } }); } @Override public void setProgressVisibility(boolean visible) { if (mProgressBar == null) { return; } mProgressBar.setVisibility( visible && (mStreamType != MediaInfo.STREAM_TYPE_LIVE) ? View.VISIBLE : View.INVISIBLE); } @Override public void setUpcomingVisibility(boolean visible) { mUpcomingContainer.setVisibility(visible ? View.VISIBLE : View.GONE); setProgressVisibility(!visible); } @Override public void setUpcomingItem(MediaQueueItem item) { mUpcomingItem = item; if (item != null) { MediaInfo mediaInfo = item.getMedia(); if (mediaInfo != null) { MediaMetadata metadata = mediaInfo.getMetadata(); setUpcomingTitle(metadata.getString(MediaMetadata.KEY_TITLE)); setUpcomingIcon(Utils.getImageUri(mediaInfo, 0)); } } else { setUpcomingTitle(""); setUpcomingIcon((Uri) null); } } @Override public void setCurrentVisibility(boolean visible) { mMainContainer.setVisibility(visible ? View.VISIBLE : View.GONE); } private void setUpCallbacks() { mPlayPause.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mListener != null) { setLoadingVisibility(true); try { mListener.onPlayPauseClicked(v); } catch (CastException e) { mListener.onFailed(R.string.ccl_failed_perform_action, UNDEFINED_STATUS_CODE); } catch (TransientNetworkDisconnectionException e) { mListener.onFailed(R.string.ccl_failed_no_connection_trans, UNDEFINED_STATUS_CODE); } catch (NoConnectionException e) { mListener .onFailed(R.string.ccl_failed_no_connection, UNDEFINED_STATUS_CODE); } } } }); mMainContainer.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mListener != null) { setLoadingVisibility(false); try { mListener.onTargetActivityInvoked(mIcon.getContext()); } catch (Exception e) { mListener.onFailed(R.string.ccl_failed_perform_action, -1); } } } }); mUpcomingPlay.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mListener != null) { mListener.onUpcomingPlayClicked(v, mUpcomingItem); } } }); mUpcomingStop.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mListener != null) { mListener.onUpcomingStopClicked(v, mUpcomingItem); } } }); } public MiniController(Context context) { super(context); loadViews(); } @Override public final void setIcon(Bitmap bm) { mIcon.setImageBitmap(bm); } private void setUpcomingIcon(Bitmap bm) { mUpcomingIcon.setImageBitmap(bm); } @Override public void setIcon(Uri uri) { if (mIconUri != null && mIconUri.equals(uri)) { return; } mIconUri = uri; if (mFetchBitmapTask != null) { mFetchBitmapTask.cancel(true); } mFetchBitmapTask = new FetchBitmapTask() { @Override protected void onPostExecute(Bitmap bitmap) { if (bitmap == null) { bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.album_art_placeholder); } setIcon(bitmap); if (this == mFetchBitmapTask) { mFetchBitmapTask = null; } } }; mFetchBitmapTask.execute(uri); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mAutoSetup && !isInEditMode()) { mCastManager.addMiniController(this); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mFetchBitmapTask != null) { mFetchBitmapTask.cancel(true); mFetchBitmapTask = null; } if (mAutoSetup && !isInEditMode()) { mCastManager.removeMiniController(this); } } @Override public void setTitle(String title) { mTitle.setText(title); } @Override public void setSubtitle(String subtitle) { mSubTitle.setText(subtitle); } @Override public void setPlaybackStatus(int state, int idleReason) { switch (state) { case MediaStatus.PLAYER_STATE_PLAYING: mPlayPause.setVisibility(View.VISIBLE); mPlayPause.setImageDrawable(getPauseStopDrawable()); setLoadingVisibility(false); break; case MediaStatus.PLAYER_STATE_PAUSED: mPlayPause.setVisibility(View.VISIBLE); mPlayPause.setImageDrawable(mPlayDrawable); setLoadingVisibility(false); break; case MediaStatus.PLAYER_STATE_IDLE: switch (mStreamType) { case MediaInfo.STREAM_TYPE_BUFFERED: mPlayPause.setVisibility(View.INVISIBLE); setLoadingVisibility(false); break; case MediaInfo.STREAM_TYPE_LIVE: if (idleReason == MediaStatus.IDLE_REASON_CANCELED) { mPlayPause.setVisibility(View.VISIBLE); mPlayPause.setImageDrawable(mPlayDrawable); setLoadingVisibility(false); } else { mPlayPause.setVisibility(View.INVISIBLE); setLoadingVisibility(false); } break; } break; case MediaStatus.PLAYER_STATE_BUFFERING: mPlayPause.setVisibility(View.INVISIBLE); setLoadingVisibility(true); break; default: mPlayPause.setVisibility(View.INVISIBLE); setLoadingVisibility(false); break; } } @Override public boolean isVisible() { return isShown(); } private void loadViews() { mIcon = (ImageView) findViewById(R.id.icon_view); mTitle = (TextView) findViewById(R.id.title_view); mSubTitle = (TextView) findViewById(R.id.subtitle_view); mPlayPause = (ImageView) findViewById(R.id.play_pause); mLoading = (ProgressBar) findViewById(R.id.loading_view); mMainContainer = findViewById(R.id.container_current); mProgressBar = (ProgressBar) findViewById(R.id.progressBar); mUpcomingIcon = (ImageView) findViewById(R.id.icon_view_upcoming); mUpcomingTitle = (TextView) findViewById(R.id.title_view_upcoming); mUpcomingContainer = findViewById(R.id.container_upcoming); mUpcomingPlay = findViewById(R.id.play_upcoming); mUpcomingStop = findViewById(R.id.stop_upcoming); } private void setLoadingVisibility(boolean show) { mLoading.setVisibility(show ? View.VISIBLE : View.GONE); } private Drawable getPauseStopDrawable() { switch (mStreamType) { case MediaInfo.STREAM_TYPE_BUFFERED: return mPauseDrawable; case MediaInfo.STREAM_TYPE_LIVE: return mStopDrawable; default: return mPauseDrawable; } } private void setUpcomingIcon(Uri uri) { if (mUpcomingIconUri != null && mUpcomingIconUri.equals(uri)) { return; } mUpcomingIconUri = uri; if (mFetchUpcomingBitmapTask != null) { mFetchUpcomingBitmapTask.cancel(true); } mFetchUpcomingBitmapTask = new FetchBitmapTask() { @Override protected void onPostExecute(Bitmap bitmap) { if (bitmap == null) { bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.album_art_placeholder); } setUpcomingIcon(bitmap); if (this == mFetchUpcomingBitmapTask) { mFetchUpcomingBitmapTask = null; } } }; mFetchUpcomingBitmapTask.execute(uri); } private void setUpcomingTitle(String title) { mUpcomingTitle.setText(title); } /** * The interface for a listener that will be called when user interacts with the * {@link MiniController}, like clicking on the play/pause button, etc. */ public interface OnMiniControllerChangedListener extends OnFailedListener { /** * Notification that user has clicked on the Play/Pause button * * @throws TransientNetworkDisconnectionException * @throws NoConnectionException * @throws CastException */ void onPlayPauseClicked(View v) throws CastException, TransientNetworkDisconnectionException, NoConnectionException; /** * Notification that the user has clicked on the album art * * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ void onTargetActivityInvoked(Context context) throws TransientNetworkDisconnectionException, NoConnectionException; /** * Called when the "play" button in the upcoming area is clicked. */ void onUpcomingPlayClicked(View v, MediaQueueItem upcomingItem); /** * Called when the "stop" button in the upcoming area is clicked. */ void onUpcomingStopClicked(View view, MediaQueueItem upcomingItem); } }