Java tutorial
package air.com.snagfilms.cast.chromecast; /* * Copyright (C) 2013 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. */ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.json.JSONObject; import air.com.snagfilms.app.exceptions.CastException; import air.com.snagfilms.app.exceptions.NoConnectionException; import air.com.snagfilms.app.exceptions.TransientNetworkDisconnectionException; import air.com.snagfilms.app.videoplayer.VideoPlayerActivity; import air.com.snagfilms.cast.chromecast.Remote.RemoteControlClientCompat; import air.com.snagfilms.cast.chromecast.Remote.RemoteControlHelper; import air.com.snagfilms.cast.chromecast.callbacks.IVideoCastConsumer; import air.com.snagfilms.cast.chromecast.callbacks.VideoCastConsumerImpl; import air.com.snagfilms.cast.chromecast.dialog.VideoMediaRouteDialogFactory; import air.com.snagfilms.cast.chromecast.notifications.VideoCastNotificationService; import air.com.snagfilms.cast.chromecast.notifications.VideoIntentReceiver; import air.com.snagfilms.cast.chromecast.utils.Utils; import air.com.snagfilms.cast.chromecast.widget.IMiniController; import air.com.snagfilms.cast.chromecast.widget.MiniController; import air.com.snagfilms.cast.chromecast.widget.MiniController.OnMiniControllerChangedListener; import air.com.snagfilms.constants.Constants; import android.annotation.SuppressLint; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Resources.NotFoundException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.AudioManager; import android.media.MediaMetadataRetriever; import android.media.RemoteControlClient; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v7.app.MediaRouteDialogFactory; import android.support.v7.media.MediaRouter.RouteInfo; import android.text.TextUtils; import android.util.Log; import android.view.View; import com.google.android.gms.cast.ApplicationMetadata; import com.google.android.gms.cast.Cast; import com.google.android.gms.cast.Cast.CastOptions.Builder; import com.google.android.gms.cast.Cast.MessageReceivedCallback; import com.google.android.gms.cast.CastDevice; import com.google.android.gms.cast.CastStatusCodes; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.RemoteMediaPlayer; import com.google.android.gms.cast.RemoteMediaPlayer.MediaChannelResult; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesUtil; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.Status; import com.google.android.gms.common.images.WebImage; import com.snagfilms.filmrise.R; /** * A concrete subclass of {@link BaseChromeCastManager} that is suitable for * casting video contents (it also provides a single custom data * channel/namespace if an out-of-bound communication is needed). * <p> * This is a singleton that needs to be "initialized" (by calling * <code>initialize()</code>) prior to usage. Subsequent to initialization, an * easier way to get access to the singleton class is to call a variant of * <code>getInstance()</code>. After initialization, callers can enable a number * of features (all features are off by default). To do so, call * <code>enableFeature()</code> and pass an OR-ed expression built from one ore * more of the following constants: * <p> * <ul> * <li>FEATURE_DEBUGGING: to enable GMS level logging</li> * <li>FEATURE_NOTIFICATION: to enable system notifications</li> * <li>FEATURE_LOCKSCREEN: to enable lock-screen controls on supported versions</li> * </ul> * Callers can add {@link MiniController} components to their application pages * by adding the corresponding widget to their layout xml and then calling * <code>addMiniController()</code>. This class manages various states of the * remote cast device.Client applications, however, can complement the default * behavior of this class by hooking into various callbacks that it provides * (see {@link IVideoCastConsumer}). Since the number of these callbacks is * usually much larger than what a single application might be interested in, * there is a no-op implementation of this interface (see * {@link VideoCastConsumerImpl}) that applications can subclass to override * only those methods that they are interested in. Since this library depends on * the cast functionalities provided by the Google Play services, the library * checks to ensure that the right version of that service is installed. It also * provides a simple static method <code>checkGooglePlaySevices()</code> that * clients can call at an early stage of their applications to provide a dialog * for users if they need to update/activate their GMS library. To learn more * about this library, please read the documentation that is distributed as part * of this library. */ public class VideoChromeCastManager extends BaseChromeCastManager implements OnMiniControllerChangedListener { public static final String EXTRA_HAS_AUTH = "hasAuth"; public static final String EXTRA_MEDIA = "media"; public static final String EXTRA_START_POINT = "startPoint"; public static final String EXTRA_SHOULD_START = "shouldStart"; public static final String EXTRA_CUSTOM_DATA = "customData"; private static final int STOP_NOTIF_WHAT = 0; private static final int START_NOTIF_WHAT = 1; private static final int NOTIF_DELAY_MS = 300; /** * Volume can be controlled at two different layers, one is at the "stream" * level and one at the "device" level. <code>VolumeType</code> encapsulates * these two types. */ public static enum VolumeType { STREAM, DEVICE; } private static final String TAG = VideoChromeCastManager.class.getName(); private static VideoChromeCastManager sInstance; private final Class<?> mTargetActivity; private final AudioManager mAudioManager; private RemoteMediaPlayer mRemoteMediaPlayer; private VolumeType mVolumeType = VolumeType.DEVICE; private int mState = MediaStatus.PLAYER_STATE_IDLE; private int mIdleReason; private Bitmap mVideoArtBitmap; private final String mDataNamespace; private Cast.MessageReceivedCallback mDataChannel; private Set<IVideoCastConsumer> mVideoConsumers; // / private IMediaAuthService mAuthService; private Handler mHandler; private int mCapabilities; private final Set<IMiniController> mMiniControllers; private ComponentName mMediaButtonReceiverComponent; private RemoteControlClientCompat mRemoteControlClientCompat; /** * Initializes the VideoChromeCastManager for clients. Before clients can * use VideoChromeCastManager, they need to initialize it by calling this * static method. Then clients can obtain an instance of this singleton * class by calling {@link getInstance()} or {@link getInstance(Context)}. * Failing to initialize this class before requesting an instance will * result in a {@link CastException} exception. * * @param context * @param applicationId * the unique ID for your application * @param targetActivity * this points to the activity that should be invoked when user * clicks on the icon in the {@link MiniController}. Often this * is the activity that hosts the local player. * @param dataNamespace * if not <code>null</code>, a custom data channel with this * namespace will be created. * @return * @see getInstance() */ public static synchronized VideoChromeCastManager initialize(Context context, String applicationId, Class<?> targetActivity, String dataNamespace) { if (null == sInstance) { Log.d(TAG, "New instance of VideoChromeCastManager is created"); if (ConnectionResult.SUCCESS != GooglePlayServicesUtil.isGooglePlayServicesAvailable(context)) { String msg = "Couldn't find the appropriate version of Goolge Play Services"; Log.e(TAG, msg); } sInstance = new VideoChromeCastManager(context, applicationId, targetActivity, dataNamespace); mCastManager = sInstance; } return sInstance; } /** * Returns the initialized instances of this class. If it is not initialized * yet, a {@link CastException} will be thrown. * * @return * @throws CastException * @see initialze() */ public static VideoChromeCastManager getInstance() throws CastException { if (null == sInstance) { Log.e(TAG, "No VideoChromeCastManager instance was built, you need to build one first"); throw new CastException(); } return sInstance; } /** * Returns the initialized instances of this class. If it is not initialized * yet, a {@link CastException} will be thrown. The {@link Context} that is * passed as the argument will be used to update the context for the * <code>VideoChromeCastManager</code> instance. The main purpose of * updating context is to enable the library to provide {@link Context} * related functionalities, e.g. it can create an error dialog if needed. * This method is preferred over the similar one without a context argument. * * @param context * the current Context * @return * @throws CastException * @see {@link initialize()}, {@link setContext()} */ public static VideoChromeCastManager getInstance(Context context) throws CastException { if (null == sInstance) { Log.e(TAG, "No VideoChromeCastManager instance was built, you need to build one first " + "(called from Context: " + context + ")"); throw new CastException(); } Log.d(TAG, "Updated context to: " + context.getClass().getName()); sInstance.mContext = context; return sInstance; } private VideoChromeCastManager(Context context, String applicationId, Class<?> targetActivity, String dataNamespace) { super(context, applicationId); mContext = context; Log.d(TAG, "VideoChromeCastManager is instantiated"); mVideoConsumers = Collections.synchronizedSet(new HashSet<IVideoCastConsumer>()); mDataNamespace = dataNamespace; if (null == targetActivity) { targetActivity = VideoPlayerActivity.class; } mTargetActivity = targetActivity; Utils.saveStringToPreference(context, Constants.PREFS_KEY_CAST_ACTIVITY_NAME, mTargetActivity.getName()); mMiniControllers = Collections.synchronizedSet(new HashSet<IMiniController>()); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mMediaButtonReceiverComponent = new ComponentName(context, VideoIntentReceiver.class); mHandler = new Handler(new UpdateNotificationHandlerCallback()); } public void setContext(Context context) { mContext = context; } /* * ========================================================================== * ================== */ /* * ========== MiniControllers managemen * ======================================================= */ /* * ========================================================================== * ================== */ /** * Updates the information and state of a MiniController. * * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ private void updateMiniController(IMiniController controller) throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); checkRemoteMediaPlayerAvailable(); if (mRemoteMediaPlayer.getStreamDuration() > 0) { MediaInfo mediaInfo = getRemoteMediaInformation(); MediaMetadata mm = mediaInfo.getMetadata(); controller.setStreamType(mediaInfo.getStreamType()); controller.setPlaybackStatus(mState, mIdleReason); controller.setSubTitle(mContext.getResources().getString(R.string.casting_to_device, mDeviceName)); controller.setTitle(mm.getString(MediaMetadata.KEY_TITLE)); if (!mm.getImages().isEmpty()) { controller.setIcon(mm.getImages().get(0).getUrl()); } } } /* * Updates the information and state of all MiniControllers */ private void updateMiniControllers() { if (null != mMiniControllers && !mMiniControllers.isEmpty()) { synchronized (mMiniControllers) { for (final IMiniController controller : mMiniControllers) { try { updateMiniController(controller); } catch (Exception e) { /* silent failure */ } } } } } /* * (non-Javadoc) * * @see com.google.sample.castcompanionlibrary.widgets.MiniController. * OnMiniControllerChangedListener#onPlayPauseClicked(android.view.View) * * @throws TransientNetworkDisconnectionException * * @throws NoConnectionException * * @throws CastException */ /** * Turns on configurable features in the library. All the supported features * are turned off by default and clients, prior to using them, need to turn * them on; it is best to do is immediately after initialization of the * library. Bitwise OR combination of features should be passed in if * multiple features are needed * <p/> * Current set of configurable features are: * <ul> * <li>FEATURE_DEBUGGING : turns on debugging in Google Play services * <li>FEATURE_NOTIFICATION : turns notifications on * <li>FEATURE_LOCKSCREEN : turns on Lock Screen using * {@link RemoteControlClient} in supported versions (JB+) * </ul> * * @param capabilities */ public void enableFeatures(int capabilities) { mCapabilities = capabilities; } /* * Returns true if and only if the feature is turned on */ protected boolean isFeatureEnabled(int feature) { return (feature & mCapabilities) > 0; } /* * (non-Javadoc) * * @see com.google.sample.castcompanionlibrary.widgets.MiniController. * OnMiniControllerChangedListener#onPlayPauseClicked(android.view.View) * * @throws TransientNetworkDisconnectionException * * @throws NoConnectionException * * @throws CastException */ // @Override public void onPlayPauseClicked(View v) throws CastException, TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); if (mState == MediaStatus.PLAYER_STATE_PLAYING) { pause(); } else { boolean isLive = isRemoteStreamLive(); if ((mState == MediaStatus.PLAYER_STATE_PAUSED && !isLive) || (mState == MediaStatus.PLAYER_STATE_IDLE && isLive)) { play(); } } } /* * (non-Javadoc) * * @see com.google.sample.castcompanionlibrary.widgets.MiniController. * OnMiniControllerChangedListener * #onTargetActivityInvoked(android.content.Context) */ // @Override public void onTargetActivityInvoked(Context ctx) throws TransientNetworkDisconnectionException, NoConnectionException { Intent intent = new Intent(ctx, mTargetActivity); intent.putExtra(EXTRA_MEDIA, Utils.fromMediaInfo(getRemoteMediaInformation())); ctx.startActivity(intent); } /** * Updates the visibility of the mini controllers. In most cases, clients do * not need to use this as the {@link VideoChromeCastManager} handles the * visibility. * * @param visible */ public void updateMiniControllersVisibility(boolean visible) { Log.d(TAG, "updateMiniControllersVisibility() reached with visibility: " + visible); if (null != mMiniControllers) { synchronized (mMiniControllers) { for (IMiniController controller : mMiniControllers) { controller.setVisibility(visible ? View.VISIBLE : View.GONE); } } } } /* * ========================================================================== * ================== */ /* * ========== VideoCastControllerActivity management * ========================================== */ /* * ========================================================================== * ================== */ /** * Launches the VideoCastControllerActivity that provides a default Cast * Player page. * * @param context * @param mediaWrapper * a bundle wrapper for the media that is or will be casted * @param position * (in milliseconds) is the starting point of the media playback * @param shouldStart * indicates if the remote playback should start after launching * the new page * @param customData * Optional {@link JSONObject} */ public void startCastControllerActivity(Context context, Bundle mediaWrapper, int position, boolean shouldStart, JSONObject customData) { Intent intent = new Intent(context, VideoPlayerActivity.class); intent.putExtra(EXTRA_MEDIA, mediaWrapper); intent.putExtra(EXTRA_START_POINT, position); intent.putExtra(EXTRA_SHOULD_START, shouldStart); if (null != customData) { intent.putExtra(EXTRA_CUSTOM_DATA, customData.toString()); } context.startActivity(intent); } /** * Launches the {@link VideoCastControllerActivity} that provides a default * Cast Player page. * * @param context * @param mediaWrapper * a bundle wrapper for the media that is or will be casted * @param position * (in milliseconds) is the starting point of the media playback * @param shouldStart * indicates if the remote playback should start after launching * the new page */ public void startCastControllerActivity(Context context, Bundle mediaWrapper, int position, boolean shouldStart) { Intent intent = new Intent(context, VideoPlayerActivity.class); intent.putExtra(EXTRA_MEDIA, mediaWrapper); intent.putExtra(EXTRA_START_POINT, position); intent.putExtra(EXTRA_SHOULD_START, shouldStart); context.startActivity(intent); } /** * Launches the {@link VideoCastControllerActivity} that provides a default * Cast Player page. This variation should be used when an * {@link IMediaAuthService} needs to be used. * * @param context * @param authService */ /* * public void startCastControllerActivity(Context context, * IMediaAuthService authService) { if (null != authService) { * this.mAuthService = authService; Intent intent = new Intent(context, * VideoCastControllerActivity.class); intent.putExtra(EXTRA_HAS_AUTH, * true); context.startActivity(intent); } } */ /** * Launches the {@link VideoCastControllerActivity} that provides a default * Cast Player page. * * @param ctx * @param mediaInfo * pointing to the media that is or will be casted * @param position * (in milliseconds) is the starting point of the media playback * @param shouldStart * indicates if the remote playback should start after launching * the new page */ public void startCastControllerActivity(Context ctx, MediaInfo mediaInfo, int position, boolean shouldStart) { startCastControllerActivity(ctx, Utils.fromMediaInfo(mediaInfo), position, shouldStart); } /** * Returns the instance of {@link IMediaAuthService}, or null if there is no * such instance. * * @return */ /* * public IMediaAuthService getMediaAuthService() { return mAuthService; } */ /** * Removes the pointer to the {@link IMediaAuthService} so it can be GC. */ /* * public void removeMediaAuthService() { mAuthService = null; } */ /* * ========================================================================== * ================== */ /* * ========== Utility Methods * ================================================================= */ /* * ========================================================================== * ================== */ /** * Returns the active {@link RemoteMediaPlayer} instance. Since there are a * number of media control APIs that this library do not provide a wrapper * for, client applications can call those methods directly after obtaining * an instance of the active {@link RemoteMediaPlayer}. * * @return */ public final RemoteMediaPlayer getRemoteMediaPlayer() { return mRemoteMediaPlayer; } /** * Determines if the media that is loaded remotely is a live stream or not. * * @return * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public final boolean isRemoteStreamLive() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); MediaInfo info = getRemoteMediaInformation(); return null != info && info.getStreamType() == MediaInfo.STREAM_TYPE_LIVE; } /** * A helper method to determine if, given a player state and an idle reason * (if the state is idle) will warrant having a UI for remote presentation * of the remote content. * * @param state * @param idleReason * @return * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public boolean shouldRemoteUiBeVisible(int state, int idleReason) throws TransientNetworkDisconnectionException, NoConnectionException { switch (state) { case MediaStatus.PLAYER_STATE_PLAYING: case MediaStatus.PLAYER_STATE_PAUSED: case MediaStatus.PLAYER_STATE_BUFFERING: return true; case MediaStatus.PLAYER_STATE_IDLE: if (!isRemoteStreamLive()) { return false; } return idleReason == MediaStatus.IDLE_REASON_CANCELED; default: break; } return false; } /* * A simple check to make sure mRemoteMediaPlayer is not null */ private void checkRemoteMediaPlayerAvailable() throws NoConnectionException { if (null == mRemoteMediaPlayer) { throw new NoConnectionException(); } } /** * Returns the bitmap for the current video * * @return */ public Bitmap getAlbumArt() { return mVideoArtBitmap; } /** * Sets the type of volume. * * @param vType */ public final void setVolumeType(VolumeType vType) { mVolumeType = vType; } /** * Returns the url for the movie that is currently playing on the remote * device. If there is no connection, this will return <code>null</code>. * * @return * @throws NoConnectionException * If no connectivity to the device exists * @throws TransientNetworkDisconnectionException * If framework is still trying to recover from a possibly * transient loss of network */ public String getRemoteMovieUrl() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); if (null != mRemoteMediaPlayer && null != mRemoteMediaPlayer.getMediaInfo()) { MediaInfo info = mRemoteMediaPlayer.getMediaInfo(); mRemoteMediaPlayer.getMediaStatus().getPlayerState(); return info.getContentId(); } else { throw new NoConnectionException(); } } /** * Indicates if the remote movie is currently playing (or buffering). * * @return * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public boolean isRemoteMoviePlaying() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); return mState == MediaStatus.PLAYER_STATE_BUFFERING || mState == MediaStatus.PLAYER_STATE_PLAYING; } /** * Returns <code>true</code> if the remote connected device is playing a * movie. * * @return * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public boolean isRemoteMoviePaused() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); return mState == MediaStatus.PLAYER_STATE_PAUSED; } /** * Returns <code>true</code> only if there is a media on the remote being * played, paused or buffered. * * @return * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public boolean isRemoteMediaLoaded() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); return isRemoteMoviePaused() || isRemoteMoviePlaying(); } /** * Returns the {@link MediaInfo} for the current media * * @return * @throws NoConnectionException * If no connectivity to the device exists * @throws TransientNetworkDisconnectionException * If framework is still trying to recover from a possibly * transient loss of network */ public MediaInfo getRemoteMediaInformation() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); checkRemoteMediaPlayerAvailable(); return mRemoteMediaPlayer.getMediaInfo(); } /** * Gets the remote's system volume. If no device is connected to, or if an * exception is thrown, this returns -1. It internally detects what type of * volume is used. * * @throws NoConnectionException * If no connectivity to the device exists * @throws TransientNetworkDisconnectionException * If framework is still trying to recover from a possibly * transient loss of network */ public double getVolume() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); if (mVolumeType == VolumeType.STREAM) { checkRemoteMediaPlayerAvailable(); return mRemoteMediaPlayer.getMediaStatus().getStreamVolume(); } else { return Cast.CastApi.getVolume(mApiClient); } } /** * Sets the volume. It internally determines if this should be done for * <code>stream</code> or <code>device</code> volume. * * @param volume * Should be a value between 0 and 1, inclusive. * @throws NoConnectionException * @throws TransientNetworkDisconnectionException * @throws CastException * If setting system volume fails */ public void setVolume(double volume) throws CastException, TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); if (volume > 1.0) { volume = 1.0; } else if (volume < 0) { volume = 0.0; } if (mVolumeType == VolumeType.STREAM) { checkRemoteMediaPlayerAvailable(); mRemoteMediaPlayer.setStreamVolume(mApiClient, volume) .setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() { @Override public void onResult(MediaChannelResult result) { if (!result.getStatus().isSuccess()) { onFailed(R.string.failed_setting_volume, result.getStatus().getStatusCode()); } } }); } else { try { Cast.CastApi.setVolume(mApiClient, volume); } catch (IOException e) { throw new CastException(e); } catch (IllegalStateException e) { throw new CastException(e); } catch (IllegalArgumentException e) { throw new CastException(e); } } } /** * Increments (or decrements) the volume by the given amount. It internally * determines if this should be done for <code>stream</code> or * <code>device</code> volume. * * @param delta * @throws NoConnectionException * If no connectivity to the device exists * @throws TransientNetworkDisconnectionException * If framework is still trying to recover from a possibly * transient loss of network * @throws CastException */ public void incrementVolume(double delta) throws CastException, TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); double vol = getVolume() + delta; if (vol > 1) { vol = 1; } else if (vol < 0) { vol = 0; } setVolume(vol); } /** * Increments or decrements volume by <code>delta</code> if * <code>delta > 0</code> or <code>delta < 0</code>, respectively. * Note that the volume range is between 0 and {@link * RouteInfo.getVolumeMax()}. * * @param delta */ public void updateVolume(int delta) { if (null != mMediaRouter.getSelectedRoute()) { RouteInfo info = mMediaRouter.getSelectedRoute(); info.requestUpdateVolume(delta); } } /** * Returns <code>true</code> if remote device is muted. It internally * determines if this should be done for <code>stream</code> or * <code>device</code> volume. * * @return * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public boolean isMute() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); if (mVolumeType == VolumeType.STREAM) { checkRemoteMediaPlayerAvailable(); return mRemoteMediaPlayer.getMediaStatus().isMute(); } else { return Cast.CastApi.isMute(mApiClient); } } /** * Mutes or un-mutes the volume. It internally determines if this should be * done for <code>stream</code> or <code>device</code> volume. * * @param mute * @throws CastException * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void setMute(boolean mute) throws CastException, TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); if (mVolumeType == VolumeType.STREAM) { checkRemoteMediaPlayerAvailable(); mRemoteMediaPlayer.setStreamMute(mApiClient, mute); } else { try { Cast.CastApi.setMute(mApiClient, mute); } catch (Exception e) { Log.e(TAG, "Failed to set volume", e); throw new CastException("Failed to set volume", e); } } } /** * Returns the duration of the media that is loaded, in milliseconds. * * @return * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public long getMediaDuration() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); checkRemoteMediaPlayerAvailable(); return mRemoteMediaPlayer.getStreamDuration(); } /** * Returns the current (approximate) position of the current media, in * milliseconds. * * @return * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public long getCurrentMediaPosition() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); checkRemoteMediaPlayerAvailable(); return mRemoteMediaPlayer.getApproximateStreamPosition(); } /** * Returns the target activity that points to the player page * * @return */ public Class<?> getTargetActivity() { return mTargetActivity; } /* * This is called when ui visibility of the client has changed */ @Override protected void onUiVisibilityChanged(boolean visible) { if (visible) { mHandler.removeMessages(STOP_NOTIF_WHAT); } mHandler.sendEmptyMessageDelayed(visible ? START_NOTIF_WHAT : STOP_NOTIF_WHAT, NOTIF_DELAY_MS); super.onUiVisibilityChanged(visible); } /* * ========================================================================== * ================== */ /* * ========== Notification Service * ============================================================ */ /* * ========================================================================== * ================== */ /* * Starts a service that can last beyond the lifetime of the application to * provide notifications. The service brings itself down when needed. The * service will be started only if the notification feature has been enabled * during the initialization. * * @see {@link BaseChromeCastManager#enableFeatures()} */ private boolean startNotificationService() { if (!isFeatureEnabled(FEATURE_NOTIFICATION)) { return true; } Log.d(TAG, "startNotificationService() "); Intent service = new Intent(mContext, VideoCastNotificationService.class); service.setPackage(mContext.getPackageName()); return null != mContext.startService(service); } private void stopNotificationService() { if (!isFeatureEnabled(FEATURE_NOTIFICATION)) { return; } if (null != mContext) { mContext.stopService(new Intent(mContext, VideoCastNotificationService.class)); } } /* * ========================================================================== * ================== */ /* * ========== Implementing Cast.Listener * ====================================================== */ /* * ========================================================================== * ================== */ private void onApplicationDisconnected(int errorCode) { Log.d(TAG, "onApplicationDisconnected() reached with error code: " + errorCode); updateRemoteControl(false); synchronized (mVideoConsumers) { for (IVideoCastConsumer consumer : mVideoConsumers) { try { consumer.onApplicationDisconnected(errorCode); } catch (Exception e) { Log.e(TAG, "onApplicationDisconnected(): Failed to inform " + consumer, e); } } } if (null != mMediaRouter) { mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute()); } onDeviceSelected(null); updateMiniControllersVisibility(false); stopNotificationService(); } private void onApplicationStatusChanged() { String appStatus = null; if (!isConnected()) { return; } try { appStatus = Cast.CastApi.getApplicationStatus(mApiClient); Log.d(TAG, "onApplicationStatusChanged() reached: " + Cast.CastApi.getApplicationStatus(mApiClient)); synchronized (mVideoConsumers) { for (IVideoCastConsumer consumer : mVideoConsumers) { try { consumer.onApplicationStatusChanged(appStatus); } catch (Exception e) { Log.e(TAG, "onApplicationStatusChanged(): Failed to inform " + consumer, e); } } } } catch (IllegalStateException e1) { // no use in logging this } } private void onVolumeChanged() { Log.d(TAG, "onVolumeChanged() reached"); double volume = 0; try { volume = getVolume(); boolean isMute = isMute(); synchronized (mVideoConsumers) { for (IVideoCastConsumer consumer : mVideoConsumers) { try { consumer.onVolumeChanged(volume, isMute); } catch (Exception e) { Log.e(TAG, "onVolumeChanged(): Failed to inform " + consumer, e); } } } } catch (Exception e1) { Log.e(TAG, "Failed to get volume", e1); } } @Override void onApplicationConnected(ApplicationMetadata appMetadata, String applicationStatus, String sessionId, boolean wasLaunched) { Log.d(TAG, "onApplicationConnected() reached with sessionId: " + sessionId + ", and mReconnectionStatus=" + mReconnectionStatus); if (mReconnectionStatus == ReconnectionStatus.IN_PROGRESS) { // we have tried to reconnect and successfully launched the app, so // it is time to select the route and make the cast icon happy :-) List<RouteInfo> routes = mMediaRouter.getRoutes(); if (null != routes) { String routeId = Utils.getStringFromPreference(mContext, PREFS_KEY_ROUTE_ID); for (RouteInfo routeInfo : routes) { if (routeId.equals(routeInfo.getId())) { // found the right route Log.d(TAG, "Found the correct route during reconnection attempt"); mReconnectionStatus = ReconnectionStatus.FINALIZE; mMediaRouter.selectRoute(routeInfo); break; } } } } startNotificationService(); try { attachDataChannel(); attachMediaChannel(); mSessionId = sessionId; // saving device for future retrieval; we only save the last session // info Utils.saveStringToPreference(mContext, PREFS_KEY_SESSION_ID, mSessionId); mRemoteMediaPlayer.requestStatus(mApiClient) .setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() { @Override public void onResult(MediaChannelResult result) { if (!result.getStatus().isSuccess()) { onFailed(R.string.failed_status_request, result.getStatus().getStatusCode()); } } }); synchronized (mVideoConsumers) { for (IVideoCastConsumer consumer : mVideoConsumers) { try { consumer.onApplicationConnected(appMetadata, mSessionId, wasLaunched); } catch (Exception e) { Log.e(TAG, "onApplicationConnected(): Failed to inform " + consumer, e); } } } } catch (TransientNetworkDisconnectionException e) { Log.e(TAG, "Failed to attach media/data channel due to network issues", e); onFailed(R.string.failed_no_connection_trans, Constants.NO_STATUS_CODE); } catch (NoConnectionException e) { Log.e(TAG, "Failed to attach media/data channel due to network issues", e); onFailed(R.string.failed_no_connection, Constants.NO_STATUS_CODE); } } /* * (non-Javadoc) * * @see com.google.sample.castcompanionlibrary.cast.BaseCastManager# * onConnectivityRecovered() */ @Override public void onConnectivityRecovered() { reattachMediaChannel(); reattachDataChannel(); super.onConnectivityRecovered(); } /* * (non-Javadoc) * * @see * com.google.android.gms.cast.CastClient.Listener#onApplicationStopFailed * (int) */ @Override public void onApplicationStopFailed(int errorCode) { synchronized (mVideoConsumers) { for (IVideoCastConsumer consumer : mVideoConsumers) { try { consumer.onApplicationStopFailed(errorCode); } catch (Exception e) { Log.e(TAG, "onApplicationLaunched(): Failed to inform " + consumer, e); } } } } @Override public void onApplicationConnectionFailed(int errorCode) { Log.d(TAG, "onApplicationConnectionFailed() reached with errorCode: " + errorCode); if (mReconnectionStatus == ReconnectionStatus.IN_PROGRESS) { if (errorCode == CastStatusCodes.APPLICATION_NOT_RUNNING) { // while trying to re-establish session, we // found out that the app is not running so we need // to disconnect mReconnectionStatus = ReconnectionStatus.INACTIVE; onDeviceSelected(null); } return; } else { boolean showError = false; synchronized (mVideoConsumers) { for (IVideoCastConsumer consumer : mVideoConsumers) { try { showError = showError || consumer.onApplicationConnectionFailed(errorCode); } catch (Exception e) { Log.e(TAG, "onApplicationLaunchFailed(): Failed to inform " + consumer, e); } } } if (showError) { switch (errorCode) { case CastStatusCodes.APPLICATION_NOT_FOUND: Log.d(TAG, "onApplicationConnectionFailed(): failed due to: " + "ERROR_APPLICATION_NOT_FOUND"); Utils.showErrorDialog(mContext, R.string.failed_to_find_app); break; case CastStatusCodes.TIMEOUT: Log.d(TAG, "onApplicationConnectionFailed(): failed due to: ERROR_TIMEOUT"); Utils.showErrorDialog(mContext, R.string.failed_app_launch_timeout); break; default: Log.d(TAG, "onApplicationConnectionFailed(): failed due to: errorcode=" + errorCode); Utils.showErrorDialog(mContext, R.string.failed_to_launch_app); break; } } onDeviceSelected(null); if (null != mMediaRouter) { mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute()); } } } /* * ========================================================================== * ================== */ /* * ========== Playback methods * ================================================================ */ /* * ========================================================================== * ================== */ /** * Loads a media. For this to succeed, you need to have successfully * launched the application. * * @param media * @param autoPlay * If <code>true</code>, playback starts after load * @param position * Where to start the playback (only used if autoPlay is * <code>true</code>. Units is milliseconds. * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void loadMedia(MediaInfo media, boolean autoPlay, int position) throws TransientNetworkDisconnectionException, NoConnectionException { loadMedia(media, autoPlay, position, null); } /** * Loads a media. For this to succeed, you need to have successfully * launched the application. * * @param media * @param autoPlay * If <code>true</code>, playback starts after load * @param position * Where to start the playback (only used if autoPlay is * <code>true</code>). Units is milliseconds. * @param customData * Optional {@link JSONObject} data to be passed to the cast * device * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void loadMedia(MediaInfo media, boolean autoPlay, int position, JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "loadMedia: " + media); checkConnectivity(); if (media == null) { return; } if (mRemoteMediaPlayer == null) { Log.e(TAG, "Trying to load a video with no active media session"); throw new NoConnectionException(); } mRemoteMediaPlayer.load(mApiClient, media, autoPlay, position, customData) .setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() { @Override public void onResult(MediaChannelResult result) { if (!result.getStatus().isSuccess()) { onFailed(R.string.failed_load, result.getStatus().getStatusCode()); } } }); } /** * Plays the loaded media. * * @param position * Where to start the playback. Units is milliseconds. * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void play(int position) throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); Log.d(TAG, "attempting to play media at position " + position + " seconds"); if (mRemoteMediaPlayer == null) { Log.e(TAG, "Trying to play a video with no active media session"); throw new NoConnectionException(); } seekAndPlay(position); } /** * Resumes the playback from where it was left (can be the beginning). * * @param customData * Optional {@link JSONObject} data to be passed to the cast * device * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void play(JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "play()"); checkConnectivity(); if (mRemoteMediaPlayer == null) { Log.e(TAG, "Trying to play a video with no active media session"); throw new NoConnectionException(); } mRemoteMediaPlayer.play(mApiClient).setResultCallback(new ResultCallback<MediaChannelResult>() { @Override public void onResult(MediaChannelResult result) { if (!result.getStatus().isSuccess()) { onFailed(R.string.failed_to_play, result.getStatus().getStatusCode()); } } }); } /** * Resumes the playback from where it was left (can be the beginning). * * @throws CastException * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void play() throws CastException, TransientNetworkDisconnectionException, NoConnectionException { play(null); } /** * Stops the playback of media/stream * * @param customData * Optional {@link JSONObject} * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void stop(JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "stop()"); checkConnectivity(); if (mRemoteMediaPlayer == null) { Log.e(TAG, "Trying to stop a stream with no active media session"); throw new NoConnectionException(); } mRemoteMediaPlayer.stop(mApiClient, customData).setResultCallback(new ResultCallback<MediaChannelResult>() { @Override public void onResult(MediaChannelResult result) { if (!result.getStatus().isSuccess()) { onFailed(R.string.failed_to_stop, result.getStatus().getStatusCode()); } } }); } /** * Stops the playback of media/stream * * @throws CastException * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void stop() throws CastException, TransientNetworkDisconnectionException, NoConnectionException { stop(null); } /** * Pauses the playback. * * @throws CastException * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void pause() throws CastException, TransientNetworkDisconnectionException, NoConnectionException { pause(null); } /** * Pauses the playback. * * @param customData * Optional {@link JSONObject} data to be passed to the cast * device * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void pause(JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "attempting to pause media"); checkConnectivity(); if (mRemoteMediaPlayer == null) { Log.e(TAG, "Trying to pause a video with no active media session"); throw new NoConnectionException(); } mRemoteMediaPlayer.pause(mApiClient, customData) .setResultCallback(new ResultCallback<MediaChannelResult>() { @Override public void onResult(MediaChannelResult result) { if (!result.getStatus().isSuccess()) { onFailed(R.string.failed_to_pause, result.getStatus().getStatusCode()); } } }); } /** * Seeks to the given point without changing the state of the player, i.e. * after seek is completed, it resumes what it was doing before the start of * seek. * * @param position * in milliseconds * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void seek(int position) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "attempting to seek media"); checkConnectivity(); if (mRemoteMediaPlayer == null) { Log.e(TAG, "Trying to seek a video with no active media session"); throw new NoConnectionException(); } mRemoteMediaPlayer.seek(mApiClient, position, RemoteMediaPlayer.RESUME_STATE_UNCHANGED) .setResultCallback(new ResultCallback<MediaChannelResult>() { @Override public void onResult(MediaChannelResult result) { if (!result.getStatus().isSuccess()) { onFailed(R.string.failed_seek, result.getStatus().getStatusCode()); } } }); } /** * Seeks to the given point and starts playback regardless of the starting * state. * * @param position * in milliseconds * @throws NoConnectionException * @throws TransientNetworkDisconnectionException * @throws CastException */ public void seekAndPlay(int position) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "attempting to seek media"); checkConnectivity(); if (mRemoteMediaPlayer == null) { Log.e(TAG, "Trying to seekAndPlay a video with no active media session"); throw new NoConnectionException(); } ResultCallback<MediaChannelResult> resultCallback = new ResultCallback<MediaChannelResult>() { @Override public void onResult(MediaChannelResult result) { if (!result.getStatus().isSuccess()) { onFailed(R.string.failed_seek, result.getStatus().getStatusCode()); } } }; mRemoteMediaPlayer.seek(mApiClient, position, RemoteMediaPlayer.RESUME_STATE_PLAY) .setResultCallback(resultCallback); } /** * Toggles the playback of the movie. * * @throws CastException * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void togglePlayback() throws CastException, TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); boolean isPlaying = isRemoteMoviePlaying(); if (isPlaying) { pause(); } else { if (mState == MediaStatus.PLAYER_STATE_IDLE && mIdleReason == MediaStatus.IDLE_REASON_FINISHED) { loadMedia(getRemoteMediaInformation(), true, 0); } else { play(); } } } private void attachMediaChannel() throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "attachMedia()"); checkConnectivity(); if (null == mRemoteMediaPlayer) { mRemoteMediaPlayer = new RemoteMediaPlayer(); mRemoteMediaPlayer.setOnStatusUpdatedListener(new RemoteMediaPlayer.OnStatusUpdatedListener() { @Override public void onStatusUpdated() { Log.d(TAG, "RemoteMediaPlayer::onStatusUpdated() is reached"); VideoChromeCastManager.this.onRemoteMediaPlayerStatusUpdated(); } }); mRemoteMediaPlayer.setOnMetadataUpdatedListener(new RemoteMediaPlayer.OnMetadataUpdatedListener() { @Override public void onMetadataUpdated() { Log.d(TAG, "RemoteMediaPlayer::onMetadataUpdated() is reached"); VideoChromeCastManager.this.onRemoteMediaPlayerMetadataUpdated(); } }); } try { Log.d(TAG, "Registering MediaChannel namespace"); Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mRemoteMediaPlayer.getNamespace(), mRemoteMediaPlayer); } catch (Exception e) { Log.e(TAG, "Failed to set up media channel", e); } } private void reattachMediaChannel() { if (null != mRemoteMediaPlayer && null != mApiClient) { try { Log.d(TAG, "Registering MediaChannel namespace"); Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mRemoteMediaPlayer.getNamespace(), mRemoteMediaPlayer); } catch (IOException e) { Log.e(TAG, "Failed to setup media channel", e); } catch (IllegalStateException e) { Log.e(TAG, "Failed to setup media channel", e); } } } private void detachMediaChannel() { Log.d(TAG, "trying to detach media channel"); if (null != mRemoteMediaPlayer) { if (null != mRemoteMediaPlayer && null != Cast.CastApi) { try { Cast.CastApi.removeMessageReceivedCallbacks(mApiClient, mRemoteMediaPlayer.getNamespace()); } catch (Exception e) { Log.e(TAG, "Failed to detach media channel", e); } } mRemoteMediaPlayer = null; } } /** * Returns the playback status of the remote device. * * @return Returns one of the values * <ul> * <li> <code>MediaStatus.PLAYER_STATE_PLAYING</code> * <li> <code>MediaStatus.PLAYER_STATE_PAUSED</code> * <li> <code>MediaStatus.PLAYER_STATE_BUFFERING</code> * <li> <code>MediaStatus.PLAYER_STATE_IDLE</code> * <li> <code>MediaStatus.PLAYER_STATE_UNKNOWN</code> * </ul> */ public int getPlaybackStatus() { return mState; } /** * Returns the Idle reason, defined in <code>MediaStatus.IDLE_*</code>. Note * that the returned value is only meaningful if the status is truly * <code>MediaStatus.PLAYER_STATE_IDLE * </code> * * @return */ public int getIdleReason() { return mIdleReason; } /* * ========================================================================== * ================== */ /* * ========== DataChannel callbacks and methods * =============================================== */ /* * ========================================================================== * ================== */ /* * If a data namespace was provided when initializing this class, we set * things up for a data channel * * @throws NoConnectionException * * @throws TransientNetworkDisconnectionException */ private void attachDataChannel() throws TransientNetworkDisconnectionException, NoConnectionException { if (TextUtils.isEmpty(mDataNamespace)) { return; } if (mDataChannel != null) { return; } checkConnectivity(); mDataChannel = new MessageReceivedCallback() { @Override public void onMessageReceived(CastDevice castDevice, String namespace, String message) { synchronized (mVideoConsumers) { for (IVideoCastConsumer consumer : mVideoConsumers) { try { consumer.onDataMessageReceived(message); } catch (Exception e) { Log.e(TAG, "onMessageReceived(): Failed to inform " + consumer, e); } } } } }; try { Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mDataNamespace, mDataChannel); } catch (IOException e) { Log.e(TAG, "Failed to setup data channel", e); } catch (IllegalStateException e) { Log.e(TAG, "Failed to setup data channel", e); } } private void reattachDataChannel() { if (!TextUtils.isEmpty(mDataNamespace) && null != mDataChannel && null != mApiClient) { try { Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mDataNamespace, mDataChannel); } catch (IOException e) { Log.e(TAG, "Failed to setup data channel", e); } catch (IllegalStateException e) { Log.e(TAG, "Failed to setup data channel", e); } } } private void onMessageSendFailed(int errorCode) { synchronized (mVideoConsumers) { for (IVideoCastConsumer consumer : mVideoConsumers) { try { consumer.onDataMessageSendFailed(errorCode); } catch (Exception e) { Log.e(TAG, "onMessageSendFailed(): Failed to inform " + consumer, e); } } } } /** * Sends the <code>message</code> on the data channel for the namespace that * was provided during the initialization of this class. If * <code>messageId > 0</code>, then it has to be a unique identifier for * the message; this id will be returned if an error occurs. If * <code>messageId == 0</code>, then an auto-generated unique identifier * will be created and returned for the message. * * @param message * @return * @throws IllegalStateException * If the namespace is empty or null * @throws NoConnectionException * If no connectivity to the device exists * @throws TransientNetworkDisconnectionException * If framework is still trying to recover from a possibly * transient loss of network */ public void sendDataMessage(String message) throws TransientNetworkDisconnectionException, NoConnectionException { if (TextUtils.isEmpty(mDataNamespace)) { throw new IllegalStateException("No Data Namespace is configured"); } checkConnectivity(); Cast.CastApi.sendMessage(mApiClient, mDataNamespace, message) .setResultCallback(new ResultCallback<Status>() { @Override public void onResult(Status result) { if (!result.isSuccess()) { VideoChromeCastManager.this.onMessageSendFailed(result.getStatusCode()); } } }); } /** * Remove the custom data channel, if any. It returns <code>true</code> if * it succeeds otherwise if it encounters an error or if no connection * exists or if no custom data channel exists, then it returns * <code>false</code> * * @return */ public boolean removeDataChannel() { if (TextUtils.isEmpty(mDataNamespace)) { return false; } try { if (null != Cast.CastApi && null != mApiClient) { Cast.CastApi.removeMessageReceivedCallbacks(mApiClient, mDataNamespace); } mDataChannel = null; return true; } catch (Exception e) { Log.e(TAG, "Failed to remove namespace: " + mDataNamespace, e); } return false; } /* * ========================================================================== * ================== */ /* * ========== MediaChannel callbacks * ========================================================== */ /* * ========================================================================== * ================== */ /* * This is called by onStatusUpdated() of the RemoteMediaPlayer */ private void onRemoteMediaPlayerStatusUpdated() { Log.d(TAG, "onRemoteMediaPlayerStatusUpdated() reached"); if (null == mApiClient || null == mRemoteMediaPlayer || null == mRemoteMediaPlayer.getMediaStatus()) { Log.d(TAG, "mApiClient or mRemoteMediaPlayer is null, so will not proceed"); return; } mState = mRemoteMediaPlayer.getMediaStatus().getPlayerState(); mIdleReason = mRemoteMediaPlayer.getMediaStatus().getIdleReason(); try { double volume = getVolume(); boolean isMute = isMute(); boolean makeUiHidden = false; if (mState == MediaStatus.PLAYER_STATE_PLAYING) { updateRemoteControl(true); } else if (mState == MediaStatus.PLAYER_STATE_PAUSED) { updateRemoteControl(false); } else if (mState == MediaStatus.PLAYER_STATE_IDLE) { updateRemoteControl(false); if (mIdleReason == MediaStatus.IDLE_REASON_FINISHED) { removeRemoteControlClient(); makeUiHidden = true; } else if (mIdleReason == MediaStatus.IDLE_REASON_ERROR) { // something bad happened on the cast device Log.d(TAG, "Player on the receiver has thrown an error"); makeUiHidden = true; removeRemoteControlClient(); onFailed(R.string.failed_receiver_player_error, Constants.NO_STATUS_CODE); } else if (mIdleReason == MediaStatus.IDLE_REASON_CANCELED) { makeUiHidden = !isRemoteStreamLive(); } } else if (mState == MediaStatus.PLAYER_STATE_BUFFERING) { System.out.println("status: buffering"); } else { System.out.println("status: unknown"); makeUiHidden = true; } if (makeUiHidden) { stopNotificationService(); } updateMiniControllersVisibility(!makeUiHidden); updateMiniControllers(); synchronized (mVideoConsumers) { for (IVideoCastConsumer consumer : mVideoConsumers) { try { consumer.onRemoteMediaPlayerStatusUpdated(); consumer.onVolumeChanged(volume, isMute); } catch (Exception e) { Log.e(TAG, "onRemoteMediaplayerStatusUpdated(): Failed to inform " + consumer, e); } } } } catch (TransientNetworkDisconnectionException e) { Log.e(TAG, "Failed to get volume state due to network issues", e); } catch (NoConnectionException e) { Log.e(TAG, "Failed to get volume state due to network issues", e); } } /* * This is called by onMetadataUpdated() of RemoteMediaPlayer */ public void onRemoteMediaPlayerMetadataUpdated() { Log.d(TAG, "onRemoteMediaPlayerMetadataUpdated() reached"); updateLockScreenMetadata(); synchronized (mVideoConsumers) { for (IVideoCastConsumer consumer : mVideoConsumers) { try { consumer.onRemoteMediaPlayerMetadataUpdated(); } catch (Exception e) { Log.e(TAG, "onRemoteMediaPlayerMetadataUpdated(): Failed to inform " + consumer, e); } } } updateLockScreenMetadata(); try { updateLockScreenImage(getRemoteMediaInformation()); } catch (TransientNetworkDisconnectionException e) { Log.e(TAG, "Failed to update lock screen metadaa due to a network issue", e); } catch (NoConnectionException e) { Log.e(TAG, "Failed to update lock screen metadaa due to a network issue", e); } } /* * ========================================================================== * ================== */ /* * ========== RemoteControlClient management * ================================================== */ /* * ========================================================================== * ================== */ /* * Sets up the {@link RemoteControlClient} for this application. It also * handles the audio focus. */ @SuppressLint("InlinedApi") private void setUpRemoteControl(final MediaInfo info) { if (!isFeatureEnabled(BaseChromeCastManager.FEATURE_LOCKSCREEN)) { return; } Log.d(TAG, "setupRemoteControl() was called"); mAudioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); ComponentName eventReceiver = new ComponentName(mContext, VideoIntentReceiver.class.getName()); mAudioManager.registerMediaButtonEventReceiver(eventReceiver); if (mRemoteControlClientCompat == null) { Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); intent.setComponent(mMediaButtonReceiverComponent); mRemoteControlClientCompat = new RemoteControlClientCompat( PendingIntent.getBroadcast(mContext, 0, intent, 0)); RemoteControlHelper.registerRemoteControlClient(mAudioManager, mRemoteControlClientCompat); } mRemoteControlClientCompat.addToMediaRouter(mMediaRouter); mRemoteControlClientCompat.setTransportControlFlags(RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE); if (null == info) { mRemoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); return; } else { mRemoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); } // Update the remote control's image updateLockScreenImage(info); // update the remote control's metadata updateLockScreenMetadata(); } /* * Updates lock screen image */ private void updateLockScreenImage(final MediaInfo info) { if (null == info) { return; } new Thread(new Runnable() { @Override public void run() { if (null == mRemoteControlClientCompat) { return; } try { Bitmap bm = getBitmapForLockScreen(info); if (null == bm) { return; } mRemoteControlClientCompat.editMetadata(false) .putBitmap(RemoteControlClientCompat.MetadataEditorCompat.METADATA_KEY_ARTWORK, bm) .apply(); } catch (Exception e) { Log.d(TAG, "Failed to update lock screen image", e); } } }).start(); } /* * Returns the {@link Bitmap} appropriate for the right size image for lock * screen. In ICS and JB, the image shown on the lock screen is a small size * bitmap but for KitKat, the image is a full-screen image so we need to * separately handle these two cases. */ private Bitmap getBitmapForLockScreen(MediaInfo video) { if (null == video) { return null; } URL imgUrl = null; Bitmap bm = null; List<WebImage> images = video.getMetadata().getImages(); try { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { if (images.size() > 1) { imgUrl = new URL(images.get(1).getUrl().toString()); } else if (images.size() == 1) { imgUrl = new URL(images.get(0).getUrl().toString()); } else if (null != mContext) { // we don't have a url for image so get a placeholder image // from resources } } else if (!images.isEmpty()) { imgUrl = new URL(images.get(0).getUrl().toString()); } else { // we don't have a url for image so get a placeholder image from // resources } } catch (MalformedURLException e) { Log.e(TAG, "Failed to get the url for images", e); } if (null != imgUrl) { try { bm = BitmapFactory.decodeStream(imgUrl.openStream()); } catch (IOException e) { Log.e(TAG, "Failed to decoded a bitmap for url: " + imgUrl, e); } } return bm; } /* * Updates the playback status of the RemoteControlClient */ @SuppressLint("InlinedApi") private void updateRemoteControl(boolean playing) { if (!isFeatureEnabled(FEATURE_LOCKSCREEN)) { return; } if (!isConnected()) { removeRemoteControlClient(); return; } try { if (null == mRemoteControlClientCompat && playing) { setUpRemoteControl(getRemoteMediaInformation()); } if (mRemoteControlClientCompat != null) { int playState = isRemoteStreamLive() ? RemoteControlClient.PLAYSTATE_BUFFERING : RemoteControlClient.PLAYSTATE_PLAYING; mRemoteControlClientCompat .setPlaybackState(playing ? playState : RemoteControlClient.PLAYSTATE_PAUSED); } } catch (TransientNetworkDisconnectionException e) { Log.e(TAG, "Failed to setup RCC due to network issues", e); } catch (NoConnectionException e) { Log.e(TAG, "Failed to setup RCC due to network issues", e); } } /* * On ICS and JB, lock screen metadata is one liner: Title - Album Artist - * Album. On KitKat, it has two lines: Title , Album Artist - Album */ private void updateLockScreenMetadata() { if (null == mRemoteControlClientCompat || !isFeatureEnabled(FEATURE_LOCKSCREEN)) { return; } try { // Update the remote controls MediaInfo info = getRemoteMediaInformation(); if (null == info) { return; } final MediaMetadata mm = info.getMetadata(); mRemoteControlClientCompat.editMetadata(false) .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, mm.getString(MediaMetadata.KEY_TITLE)) .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, mContext.getResources().getString(R.string.casting_to_device, getDeviceName())) .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, info.getStreamDuration()).apply(); } catch (NotFoundException e) { Log.e(TAG, "Failed to update RCC due to resource not found", e); } catch (TransientNetworkDisconnectionException e) { Log.e(TAG, "Failed to update RCC due to network issues", e); } catch (NoConnectionException e) { Log.e(TAG, "Failed to update RCC due to network issues", e); } } /* * Removes the remote control client */ private void removeRemoteControlClient() { if (isFeatureEnabled(FEATURE_LOCKSCREEN)) { mAudioManager.abandonAudioFocus(null); if (null != mRemoteControlClientCompat) { RemoteControlHelper.unregisterRemoteControlClient(mAudioManager, mRemoteControlClientCompat); mRemoteControlClientCompat = null; } } } /* * ========================================================================== * ================== */ /* * ========== Registering IVideoCastConsumer listeners * ======================================== */ /* * ========================================================================== * ================== */ /** * Registers an {@link IVideoCastConsumer} interface with this class. * Registered listeners will be notified of changes to a variety of * lifecycle and media status changes through the callbacks that the * interface provides. * * @param listener * @see VideoCastConsumerImpl */ public synchronized void addVideoCastConsumer(IVideoCastConsumer listener) { if (null != listener) { super.addBaseCastConsumer(listener); synchronized (mVideoConsumers) { mVideoConsumers.add(listener); } Log.d(TAG, "Successfully added the new CastConsumer listener " + listener); } } /** * Unregisters an {@link IVideoCastConsumer}. * * @param listener */ public synchronized void removeVideoCastConsumer(IVideoCastConsumer listener) { if (null != listener) { super.removeBaseCastConsumer(listener); synchronized (mVideoConsumers) { mVideoConsumers.remove(listener); } } } /* * ========================================================================== * ================== */ /* * ========== Registering IMiniController listeners * =========================================== */ /* * ========================================================================== * ================== */ /** * Adds a new {@link IMiniController} component. Callers need to provide * their own {@link OnMiniControllerChangedListener}. * * @param miniController * @param OnMiniControllerChangedListener * @see setOnMiniControllerChangedListener */ public void addMiniController(IMiniController miniController, OnMiniControllerChangedListener onChangedListener) { if (null != miniController) { boolean result = false; synchronized (mMiniControllers) { result = mMiniControllers.add(miniController); } if (result) { miniController .setOnMiniControllerChangedListener(null == onChangedListener ? this : onChangedListener); try { if (isConnected() && isRemoteMediaLoaded()) { updateMiniController(miniController); miniController.setVisibility(View.VISIBLE); } } catch (TransientNetworkDisconnectionException e) { Log.e(TAG, "Failed to get the status of media playback on receiver", e); } catch (NoConnectionException e) { Log.e(TAG, "Failed to get the status of media playback on receiver", e); } Log.e(TAG, "Successfully added the new MiniController " + miniController); } else { Log.e(TAG, "Attempting to adding " + miniController + " but it was already " + "registered, skipping this step"); } } } /** * Adds a new {@link IMiniController} component and assigns * {@link VideoChromeCastManager} as the * {@link OnMiniControllerChangedListener} for this component. * addMiniControlleraddMiniController * * @param miniController */ public void addMiniController(IMiniController miniController) { addMiniController(miniController, null); } /** * Removes a {@link IMiniController} listener from the list of listeners. * * @param listener */ public void removeMiniController(IMiniController listener) { if (null != listener) { synchronized (mMiniControllers) { mMiniControllers.remove(listener); } } } /* * ========================================================================== * ================== */ /* * ========== Implementing abstract methods of BaseCastManage * ================================= */ /* * ========================================================================== * ================== */ @Override void onDeviceUnselected() { stopNotificationService(); detachMediaChannel(); removeDataChannel(); mState = MediaStatus.PLAYER_STATE_IDLE; } @Override Builder getCastOptionBuilder(CastDevice device) { Builder builder = Cast.CastOptions.builder(mSelectedCastDevice, new CastListener()); if (isFeatureEnabled(FEATURE_DEBUGGING)) { builder.setVerboseLoggingEnabled(true); } return builder; } @Override public void onConnectionFailed(ConnectionResult result) { super.onConnectionFailed(result); updateRemoteControl(false); stopNotificationService(); } @Override public void onDisconnected() { super.onDisconnected(); updateMiniControllersVisibility(false); stopNotificationService(); removeRemoteControlClient(); mState = MediaStatus.PLAYER_STATE_IDLE; } @Override MediaRouteDialogFactory getMediaRouteDialogFactory() { return new VideoMediaRouteDialogFactory(); } class CastListener extends Cast.Listener { /* * (non-Javadoc) * * @see * com.google.android.gms.cast.Cast.Listener#onApplicationDisconnected * (int) */ @Override public void onApplicationDisconnected(int statusCode) { VideoChromeCastManager.this.onApplicationDisconnected(statusCode); } /* * (non-Javadoc) * * @see * com.google.android.gms.cast.Cast.Listener#onApplicationStatusChanged * () */ @Override public void onApplicationStatusChanged() { VideoChromeCastManager.this.onApplicationStatusChanged(); } @Override public void onVolumeChanged() { VideoChromeCastManager.this.onVolumeChanged(); } } @Override public void onFailed(int resourceId, int statusCode) { Log.d(TAG, "onFailed: " + mContext.getString(resourceId) + ", code: " + statusCode); super.onFailed(resourceId, statusCode); } private class UpdateNotificationHandlerCallback implements Handler.Callback { @Override public boolean handleMessage(Message msg) { boolean visibility = msg.what != START_NOTIF_WHAT; if (isFeatureEnabled(FEATURE_NOTIFICATION)) { Intent intent = new Intent(VideoCastNotificationService.ACTION_VISIBILITY); intent.setPackage(mContext.getPackageName()); intent.putExtra("visible", visibility); mContext.startService(intent); } return true; } } }