/*
* CastService
* Connect SDK
*
* Copyright (c) 2014 LG Electronics.
* Created by Hyun Kook Khang on 23 Feb 2014
*
* 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.connectsdk.service;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import com.connectsdk.core.ImageInfo;
import com.connectsdk.core.MediaInfo;
import com.connectsdk.core.Util;
import com.connectsdk.discovery.DiscoveryFilter;
import com.connectsdk.discovery.DiscoveryManager;
import com.connectsdk.service.capability.CapabilityMethods;
import com.connectsdk.service.capability.MediaControl;
import com.connectsdk.service.capability.MediaPlayer;
import com.connectsdk.service.capability.VolumeControl;
import com.connectsdk.service.capability.WebAppLauncher;
import com.connectsdk.service.capability.listeners.ResponseListener;
import com.connectsdk.service.command.ServiceCommandError;
import com.connectsdk.service.command.ServiceSubscription;
import com.connectsdk.service.command.URLServiceSubscription;
import com.connectsdk.service.config.ServiceConfig;
import com.connectsdk.service.config.ServiceDescription;
import com.connectsdk.service.sessions.CastWebAppSession;
import com.connectsdk.service.sessions.LaunchSession;
import com.connectsdk.service.sessions.LaunchSession.LaunchSessionType;
import com.connectsdk.service.sessions.WebAppSession;
import com.connectsdk.service.sessions.WebAppSession.WebAppPinStatusListener;
import com.google.android.gms.cast.ApplicationMetadata;
import com.google.android.gms.cast.Cast;
import com.google.android.gms.cast.Cast.ApplicationConnectionResult;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.LaunchOptions;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaTrack;
import com.google.android.gms.cast.RemoteMediaPlayer;
import com.google.android.gms.cast.RemoteMediaPlayer.MediaChannelResult;
import com.google.android.gms.cast.TextTrackStyle;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
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 org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
public class CastService extends DeviceService implements MediaPlayer, MediaControl, VolumeControl, WebAppLauncher {
interface ConnectionListener {
void onConnected();
}
public interface LaunchWebAppListener{
void onSuccess(WebAppSession webAppSession);
void onFailure(ServiceCommandError error);
}
// @cond INTERNAL
public static final String ID = "Chromecast";
public final static String PLAY_STATE = "PlayState";
public final static String CAST_SERVICE_VOLUME_SUBSCRIPTION_NAME = "volume";
public final static String CAST_SERVICE_MUTE_SUBSCRIPTION_NAME = "mute";
// @endcond
String currentAppId;
String launchingAppId;
GoogleApiClient mApiClient;
CastListener mCastClientListener;
ConnectionCallbacks mConnectionCallbacks;
ConnectionFailedListener mConnectionFailedListener;
CastDevice castDevice;
RemoteMediaPlayer mMediaPlayer;
Map<String, CastWebAppSession> sessions;
List<URLServiceSubscription<?>> subscriptions;
float currentVolumeLevel;
boolean currentMuteStatus;
boolean mWaitingForReconnect;
static String applicationID = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
// Queue of commands that should be sent once register is complete
CopyOnWriteArraySet<ConnectionListener> commandQueue = new CopyOnWriteArraySet<ConnectionListener>();
public CastService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) {
super(serviceDescription, serviceConfig);
mCastClientListener = new CastListener();
mConnectionCallbacks = new ConnectionCallbacks();
mConnectionFailedListener = new ConnectionFailedListener();
sessions = new HashMap<String, CastWebAppSession>();
subscriptions = new ArrayList<URLServiceSubscription<?>>();
mWaitingForReconnect = false;
}
@Override
public String getServiceName() {
return ID;
}
public static DiscoveryFilter discoveryFilter() {
return new DiscoveryFilter(ID, "Chromecast");
}
public static void setApplicationID(String id) {
applicationID = id;
}
public static String getApplicationID() {
return applicationID;
}
@Override
public CapabilityPriorityLevel getPriorityLevel(Class<? extends CapabilityMethods> clazz) {
if (clazz.equals(MediaPlayer.class)) {
return getMediaPlayerCapabilityLevel();
}
else if (clazz.equals(MediaControl.class)) {
return getMediaControlCapabilityLevel();
}
else if (clazz.equals(VolumeControl.class)) {
return getVolumeControlCapabilityLevel();
}
else if (clazz.equals(WebAppLauncher.class)) {
return getWebAppLauncherCapabilityLevel();
}
return CapabilityPriorityLevel.NOT_SUPPORTED;
}
@Override
public void connect() {
if (connected && mApiClient != null &&
mApiClient.isConnecting() && mApiClient.isConnected())
return;
if (castDevice == null) {
this.castDevice = (CastDevice) getServiceDescription().getDevice();
}
if (mApiClient == null) {
mApiClient = createApiClient();
mApiClient.connect();
}
}
protected GoogleApiClient createApiClient() {
Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions
.builder(castDevice, mCastClientListener);
return new GoogleApiClient.Builder(DiscoveryManager.getInstance().getContext())
.addApi(Cast.API, apiOptionsBuilder.build())
.addConnectionCallbacks(mConnectionCallbacks)
.addOnConnectionFailedListener(mConnectionFailedListener)
.build();
}
@Override
public void disconnect() {
if (!connected)
return;
connected = false;
mWaitingForReconnect = false;
detachMediaPlayer();
if (!commandQueue.isEmpty())
commandQueue.clear();
if (mApiClient != null && mApiClient.isConnected()) {
Cast.CastApi.leaveApplication(mApiClient);
mApiClient.disconnect();
}
mApiClient = null;
Util.runOnUI(new Runnable() {
@Override
public void run() {
if (getListener() != null) {
getListener().onDisconnect(CastService.this, null);
}
}
});
}
@Override
public MediaControl getMediaControl() {
return this;
}
@Override
public CapabilityPriorityLevel getMediaControlCapabilityLevel() {
return CapabilityPriorityLevel.HIGH;
}
@Override
public void play(final ResponseListener<Object> listener) {
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
try {
mMediaPlayer.play(mApiClient);
Util.postSuccess(listener, null);
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "Unable to play", null));
}
}
};
runCommand(connectionListener);
}
@Override
public void pause(final ResponseListener<Object> listener) {
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
try {
mMediaPlayer.pause(mApiClient);
Util.postSuccess(listener, null);
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "Unable to pause", null));
}
}
};
runCommand(connectionListener);
}
@Override
public void stop(final ResponseListener<Object> listener) {
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
try {
mMediaPlayer.stop(mApiClient);
Util.postSuccess(listener, null);
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "Unable to stop", null));
}
}
};
runCommand(connectionListener);
}
@Override
public void rewind(ResponseListener<Object> listener) {
Util.postError(listener, ServiceCommandError.notSupported());
}
@Override
public void fastForward(ResponseListener<Object> listener) {
Util.postError(listener, ServiceCommandError.notSupported());
}
@Override
public void previous(ResponseListener<Object> listener) {
Util.postError(listener, ServiceCommandError.notSupported());
}
@Override
public void next(ResponseListener<Object> listener) {
Util.postError(listener, ServiceCommandError.notSupported());
}
@Override
public void seek(final long position, final ResponseListener<Object> listener) {
if (mMediaPlayer == null || mMediaPlayer.getMediaStatus() == null) {
Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null));
return;
}
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
try {
mMediaPlayer.seek(mApiClient, position, RemoteMediaPlayer.RESUME_STATE_UNCHANGED).setResultCallback(
new ResultCallback<MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
Status status = result.getStatus();
if (status.isSuccess()) {
Util.postSuccess(listener, null);
} else {
Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status));
}
}
});
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "Unable to seek", null));
}
}
};
runCommand(connectionListener);
}
@Override
public void getDuration(final DurationListener listener) {
if (mMediaPlayer != null && mMediaPlayer.getMediaStatus() != null) {
Util.postSuccess(listener, mMediaPlayer.getStreamDuration());
}
else {
Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null));
}
}
@Override
public void getPosition(final PositionListener listener) {
if (mMediaPlayer != null && mMediaPlayer.getMediaStatus() != null) {
Util.postSuccess(listener, mMediaPlayer.getApproximateStreamPosition());
}
else {
Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null));
}
}
@Override
public MediaPlayer getMediaPlayer() {
return this;
}
@Override
public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() {
return CapabilityPriorityLevel.HIGH;
}
@Override
public void getMediaInfo(MediaInfoListener listener) {
if (mMediaPlayer == null)
return;
if (mMediaPlayer.getMediaInfo() != null) {
String url = mMediaPlayer.getMediaInfo().getContentId();
String mimeType = mMediaPlayer.getMediaInfo().getContentType();
MediaMetadata metadata = mMediaPlayer.getMediaInfo().getMetadata();
String title = null;
String description = null;
ArrayList<ImageInfo> list = null;
if (metadata != null) {
title = metadata.getString(MediaMetadata.KEY_TITLE);
description = metadata.getString(MediaMetadata.KEY_SUBTITLE);
if (metadata.getImages() != null && metadata.getImages().size() > 0) {
String iconUrl = metadata.getImages().get(0).getUrl().toString();
list = new ArrayList<ImageInfo>();
list.add(new ImageInfo(iconUrl));
}
}
MediaInfo info = new MediaInfo(url, mimeType, title, description, list);
Util.postSuccess(listener, info);
}
else {
Util.postError(listener, new ServiceCommandError(0, "Media Info is null", null));
}
}
@Override
public ServiceSubscription<MediaInfoListener> subscribeMediaInfo(
MediaInfoListener listener) {
URLServiceSubscription<MediaInfoListener> request = new URLServiceSubscription<MediaInfoListener>(this, "info", null, null);
request.addListener(listener);
addSubscription(request);
return request;
}
private void attachMediaPlayer() {
if (mMediaPlayer != null) {
return;
}
mMediaPlayer = createMediaPlayer();
mMediaPlayer.setOnStatusUpdatedListener(new RemoteMediaPlayer.OnStatusUpdatedListener() {
@Override
public void onStatusUpdated() {
if (subscriptions.size() > 0) {
for (URLServiceSubscription<?> subscription: subscriptions) {
if (subscription.getTarget().equalsIgnoreCase(PLAY_STATE)) {
for (int i = 0; i < subscription.getListeners().size(); i++) {
@SuppressWarnings("unchecked")
ResponseListener<Object> listener = (ResponseListener<Object>) subscription.getListeners().get(i);
if (mMediaPlayer != null && mMediaPlayer.getMediaStatus() != null) {
PlayStateStatus status = PlayStateStatus.convertPlayerStateToPlayStateStatus(mMediaPlayer.getMediaStatus().getPlayerState());
Util.postSuccess(listener, status);
}
}
}
}
}
}
});
mMediaPlayer.setOnMetadataUpdatedListener(new RemoteMediaPlayer.OnMetadataUpdatedListener() {
@Override
public void onMetadataUpdated() {
if (subscriptions.size() > 0) {
for (URLServiceSubscription<?> subscription : subscriptions) {
if (subscription.getTarget().equalsIgnoreCase("info")) {
for (int i = 0; i < subscription.getListeners().size(); i++) {
MediaInfoListener listener = (MediaInfoListener) subscription.getListeners().get(i);
getMediaInfo(listener);
}
}
}
}
}
});
if (mApiClient != null) {
try {
Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mMediaPlayer.getNamespace(),
mMediaPlayer);
} catch (Exception e) {
Log.w(Util.T, "Exception while creating media channel", e);
}
}
}
protected RemoteMediaPlayer createMediaPlayer() {
return new RemoteMediaPlayer();
}
private void detachMediaPlayer() {
if ((mMediaPlayer != null) && (mApiClient != null)) {
try {
Cast.CastApi.removeMessageReceivedCallbacks(mApiClient,
mMediaPlayer.getNamespace());
} catch (IOException e) {
Log.w(Util.T, "Exception while launching application", e);
}
}
mMediaPlayer = null;
}
@Override
public void displayImage(String url, String mimeType, String title,
String description, String iconSrc, LaunchListener listener) {
MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_PHOTO);
mMediaMetadata.putString(MediaMetadata.KEY_TITLE, title);
mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, description);
if (iconSrc != null) {
Uri iconUri = Uri.parse(iconSrc);
WebImage image = new WebImage(iconUri, 100, 100);
mMediaMetadata.addImage(image);
}
com.google.android.gms.cast.MediaInfo mediaInformation = new com.google.android.gms.cast.MediaInfo.Builder(url)
.setContentType(mimeType)
.setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_NONE)
.setMetadata(mMediaMetadata)
.setStreamDuration(0)
.setCustomData(null)
.build();
playMedia(mediaInformation, applicationID, listener);
}
@Override
public void displayImage(MediaInfo mediaInfo, LaunchListener listener) {
String mediaUrl = null;
String mimeType = null;
String title = null;
String desc = null;
String iconSrc = null;
if (mediaInfo != null) {
mediaUrl = mediaInfo.getUrl();
mimeType = mediaInfo.getMimeType();
title = mediaInfo.getTitle();
desc = mediaInfo.getDescription();
if (mediaInfo.getImages() != null && mediaInfo.getImages().size() > 0) {
ImageInfo imageInfo = mediaInfo.getImages().get(0);
iconSrc = imageInfo.getUrl();
}
}
displayImage(mediaUrl, mimeType, title, desc, iconSrc, listener);
}
public void playMedia(String url, String subsUrl, String mimeType, String title,
String description, String iconSrc, boolean shouldLoop,
LaunchListener listener) {
MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
mMediaMetadata.putString(MediaMetadata.KEY_TITLE, title);
mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, description);
if (iconSrc != null) {
Uri iconUri = Uri.parse(iconSrc);
WebImage image = new WebImage(iconUri, 100, 100);
mMediaMetadata.addImage(image);
}
List<MediaTrack> mediaTracks = new ArrayList<>();
if(subsUrl != null) {
MediaTrack subtitle = new MediaTrack.Builder(1, MediaTrack.TYPE_TEXT)
.setName("Subtitle")
.setSubtype(MediaTrack.SUBTYPE_SUBTITLES)
.setContentId(subsUrl)
.setContentType("text/vtt")
.setLanguage("en")
.build();
mediaTracks.add(subtitle);
}
com.google.android.gms.cast.MediaInfo mediaInformation = new com.google.android.gms.cast.MediaInfo.Builder(url)
.setContentType(mimeType)
.setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED)
.setMetadata(mMediaMetadata)
.setStreamDuration(1000)
.setCustomData(null)
.setMediaTracks(mediaTracks)
.build();
playMedia(mediaInformation, applicationID, listener);
}
@Override
public void playMedia(String url, String mimeType, String title,
String description, String iconSrc, boolean shouldLoop,
LaunchListener listener) {
playMedia(url, null, mimeType, title, description, iconSrc, shouldLoop, listener);
}
@Override
public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, LaunchListener listener) {
String mediaUrl = null;
String subsUrl = null;
String mimeType = null;
String title = null;
String desc = null;
String iconSrc = null;
if (mediaInfo != null) {
mediaUrl = mediaInfo.getUrl();
subsUrl = mediaInfo.getSubsUrl();
mimeType = mediaInfo.getMimeType();
title = mediaInfo.getTitle();
desc = mediaInfo.getDescription();
if (mediaInfo.getImages() != null && mediaInfo.getImages().size() > 0) {
ImageInfo imageInfo = mediaInfo.getImages().get(0);
iconSrc = imageInfo.getUrl();
}
}
playMedia(mediaUrl, subsUrl, mimeType, title, desc, iconSrc, shouldLoop, listener);
}
private void playMedia(final com.google.android.gms.cast.MediaInfo mediaInformation, final String mediaAppId, final LaunchListener listener) {
final ApplicationConnectionResultCallback webAppLaunchCallback = new ApplicationConnectionResultCallback(new LaunchWebAppListener() {
@Override
public void onSuccess(final WebAppSession webAppSession) {
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
try {
mMediaPlayer.load(mApiClient, mediaInformation, true).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
Status status = result.getStatus();
if (status.isSuccess()) {
webAppSession.launchSession.setSessionType(LaunchSessionType.Media);
// White text, black outline, no background
TextTrackStyle textTrackStyle = new TextTrackStyle();
textTrackStyle.setForegroundColor(Color.parseColor("#FFFFFFFF"));
textTrackStyle.setBackgroundColor(Color.parseColor("#01000000"));
textTrackStyle.setWindowType(TextTrackStyle.WINDOW_TYPE_NONE);
textTrackStyle.setEdgeType(TextTrackStyle.EDGE_TYPE_OUTLINE);
textTrackStyle.setEdgeColor(Color.BLACK);
textTrackStyle.setFontGenericFamily(TextTrackStyle.FONT_FAMILY_SANS_SERIF);
mMediaPlayer.setTextTrackStyle(mApiClient, textTrackStyle);
mMediaPlayer.setActiveMediaTracks(mApiClient, new long[]{1});
Util.postSuccess(listener, new MediaLaunchObject(webAppSession.launchSession, CastService.this));
}
else {
Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status));
}
}
});
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "Unable to load", null));
}
}
};
runCommand(connectionListener);
}
@Override
public void onFailure(ServiceCommandError error) {
Util.postError(listener, error);
}
});
launchingAppId = mediaAppId;
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
boolean relaunchIfRunning = false;
try {
if (Cast.CastApi.getApplicationStatus(mApiClient) == null || (!mediaAppId.equals(currentAppId))) {
relaunchIfRunning = true;
}
LaunchOptions options = new LaunchOptions();
options.setRelaunchIfRunning(relaunchIfRunning);
Cast.CastApi.launchApplication(mApiClient, mediaAppId, options).setResultCallback(webAppLaunchCallback);
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "Unable to launch", null));
}
}
};
runCommand(connectionListener);
}
@Override
public void closeMedia(final LaunchSession launchSession, final ResponseListener<Object> listener) {
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
try {
Cast.CastApi.stopApplication(mApiClient, launchSession.getSessionId()).setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status result) {
if (result.isSuccess()) {
Util.postSuccess(listener, result);
} else {
Util.postError(listener, new ServiceCommandError(result.getStatusCode(), result.getStatusMessage(), result));
}
}
});
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "Unable to stop", null));
}
}
};
runCommand(connectionListener);
}
@Override
public WebAppLauncher getWebAppLauncher() {
return this;
}
@Override
public CapabilityPriorityLevel getWebAppLauncherCapabilityLevel() {
return CapabilityPriorityLevel.HIGH;
}
@Override
public void launchWebApp(String webAppId, WebAppSession.LaunchListener listener) {
launchWebApp(webAppId, true, listener);
}
@Override
public void launchWebApp(final String webAppId, final boolean relaunchIfRunning, final WebAppSession.LaunchListener listener) {
launchingAppId = webAppId;
final LaunchWebAppListener launchWebAppListener = new LaunchWebAppListener() {
@Override
public void onSuccess(WebAppSession webAppSession) {
Util.postSuccess(listener, webAppSession);
}
@Override
public void onFailure(ServiceCommandError error) {
Util.postError(listener, error);
}
};
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
// TODO Workaround, for some reason, if relaunchIfRunning is false, launchApplication returns 2005 error and cannot launch.
try {
if (relaunchIfRunning == false) {
Cast.CastApi.joinApplication(mApiClient).setResultCallback(new ResultCallback<Cast.ApplicationConnectionResult>() {
@Override
public void onResult(ApplicationConnectionResult result) {
if (result.getStatus().isSuccess() &&
result.getApplicationMetadata() != null &&
result.getApplicationMetadata().getName() != null &&
result.getApplicationMetadata().getApplicationId().equals(webAppId)) {
ApplicationMetadata applicationMetadata = result.getApplicationMetadata();
currentAppId = applicationMetadata.getApplicationId();
LaunchSession launchSession = LaunchSession.launchSessionForAppId(applicationMetadata.getApplicationId());
launchSession.setAppName(applicationMetadata.getName());
launchSession.setSessionId(result.getSessionId());
launchSession.setSessionType(LaunchSessionType.WebApp);
launchSession.setService(CastService.this);
CastWebAppSession webAppSession = new CastWebAppSession(launchSession, CastService.this);
webAppSession.setMetadata(applicationMetadata);
sessions.put(applicationMetadata.getApplicationId(), webAppSession);
Util.postSuccess(listener, webAppSession);
} else {
LaunchOptions options = new LaunchOptions();
options.setRelaunchIfRunning(true);
try {
Cast.CastApi.launchApplication(mApiClient, webAppId, options).setResultCallback(
new ApplicationConnectionResultCallback(launchWebAppListener));
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "Unable to launch", null));
}
}
}
});
}
else {
LaunchOptions options = new LaunchOptions();
options.setRelaunchIfRunning(relaunchIfRunning);
Cast.CastApi.launchApplication(mApiClient, webAppId, options).setResultCallback(
new ApplicationConnectionResultCallback(launchWebAppListener)
);
}
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "Unable to launch", null));
}
}
};
runCommand(connectionListener);
}
@Override
public void launchWebApp(String webAppId, JSONObject params, WebAppSession.LaunchListener listener) {
Util.postError(listener, ServiceCommandError.notSupported());
}
@Override
public void launchWebApp(String webAppId, JSONObject params, boolean relaunchIfRunning, WebAppSession.LaunchListener listener) {
Util.postError(listener, ServiceCommandError.notSupported());
}
public void requestStatus(final ResponseListener<Object> listener) {
try {
mMediaPlayer
.requestStatus(mApiClient)
.setResultCallback(
new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
if (result.getStatus().isSuccess()) {
Util.postSuccess(listener, result);
}
else {
Util.postError(listener, new ServiceCommandError(0, "Failed to request status", result));
}
}
});
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null));
}
}
public void joinApplication(final ResponseListener<Object> listener) {
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
try {
Cast.CastApi.joinApplication(mApiClient).setResultCallback(new ResultCallback<Cast.ApplicationConnectionResult>() {
@Override
public void onResult(ApplicationConnectionResult result) {
if (result.getStatus().isSuccess()) {
// TODO: Maybe there is better way to check current cast device is showing backdrop, but for now, if chromecast is showing backdrop, then requestStatus would never response.
if (result.getApplicationMetadata() != null &&
result.getApplicationMetadata().getName() != null &&
!result.getApplicationMetadata().getName().equals("Backdrop") &&
mMediaPlayer != null && mApiClient != null) {
mMediaPlayer.requestStatus(mApiClient).setResultCallback(
new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
Util.postSuccess(listener, result);
}
});
}
else {
Util.postSuccess(listener, result);
}
}
else {
Util.postError(listener, new ServiceCommandError(0, "Failed to join application", result));
}
}
});
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "Unable to join", null));
}
}
};
runCommand(connectionListener);
}
@Override
public void joinWebApp(final LaunchSession webAppLaunchSession, final WebAppSession.LaunchListener listener) {
final ApplicationConnectionResultCallback webAppLaunchCallback = new ApplicationConnectionResultCallback(new LaunchWebAppListener() {
@Override
public void onSuccess(final WebAppSession webAppSession) {
webAppSession.connect(new ResponseListener<Object>() {
@Override
public void onSuccess(Object object) {
requestStatus(new ResponseListener<Object>() {
@Override
public void onSuccess(Object object) {
Util.postSuccess(listener, webAppSession);
}
@Override
public void onError(ServiceCommandError error) {
// we sent success, because join is already succeeded.
Util.postSuccess(listener, webAppSession);
}
});
}
@Override
public void onError(ServiceCommandError error) {
Util.postError(listener, error);
}
});
}
@Override
public void onFailure(ServiceCommandError error) {
Util.postError(listener, error);
}
});
launchingAppId = webAppLaunchSession.getAppId();
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
try {
Cast.CastApi.joinApplication(mApiClient, webAppLaunchSession.getAppId()).setResultCallback(webAppLaunchCallback);
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "Unable to join", null));
}
}
};
runCommand(connectionListener);
}
@Override
public void joinWebApp(String webAppId, WebAppSession.LaunchListener listener) {
LaunchSession launchSession = LaunchSession.launchSessionForAppId(webAppId);
launchSession.setSessionType(LaunchSessionType.WebApp);
launchSession.setService(this);
joinWebApp(launchSession, listener);
}
@Override
public void closeWebApp(LaunchSession launchSession, final ResponseListener<Object> listener) {
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
try {
Cast.CastApi.stopApplication(mApiClient).setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
if (status.isSuccess()) {
Util.postSuccess(listener, null);
}
else {
Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status));
}
}
});
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "Unable to stop", null));
}
}
};
runCommand(connectionListener);
}
@Override
public void pinWebApp(String webAppId, ResponseListener<Object> listener) {
Util.postError(listener, ServiceCommandError.notSupported());
}
@Override
public void unPinWebApp(String webAppId, ResponseListener<Object> listener) {
Util.postError(listener, ServiceCommandError.notSupported());
}
@Override
public void isWebAppPinned(String webAppId, WebAppPinStatusListener listener) {
Util.postError(listener, ServiceCommandError.notSupported());
}
@Override
public ServiceSubscription<WebAppPinStatusListener> subscribeIsWebAppPinned(
String webAppId, WebAppPinStatusListener listener) {
Util.postError(listener, ServiceCommandError.notSupported());
return null;
}
@Override
public VolumeControl getVolumeControl() {
return this;
}
@Override
public CapabilityPriorityLevel getVolumeControlCapabilityLevel() {
return CapabilityPriorityLevel.HIGH;
}
@Override
public void volumeUp(final ResponseListener<Object> listener) {
getVolume(new VolumeListener() {
@Override
public void onSuccess(final Float volume) {
if (volume >= 1.0) {
Util.postSuccess(listener, null);
}
else {
float newVolume = (float)(volume + 0.01);
if (newVolume > 1.0)
newVolume = (float)1.0;
setVolume(newVolume, listener);
Util.postSuccess(listener, null);
}
}
@Override
public void onError(ServiceCommandError error) {
Util.postError(listener, error);
}
});
}
@Override
public void volumeDown(final ResponseListener<Object> listener) {
getVolume(new VolumeListener() {
@Override
public void onSuccess(final Float volume) {
if (volume <= 0.0) {
Util.postSuccess(listener, null);
}
else {
float newVolume = (float)(volume - 0.01);
if (newVolume < 0.0)
newVolume = (float)0.0;
setVolume(newVolume, listener);
Util.postSuccess(listener, null);
}
}
@Override
public void onError(ServiceCommandError error) {
Util.postError(listener, error);
}
});
}
@Override
public void setVolume(final float volume, final ResponseListener<Object> listener) {
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
try {
Cast.CastApi.setVolume(mApiClient, volume);
Util.postSuccess(listener, null);
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "setting volume level failed", null));
}
}
};
runCommand(connectionListener);
}
@Override
public void getVolume(VolumeListener listener) {
Util.postSuccess(listener, currentVolumeLevel);
}
@Override
public void setMute(final boolean isMute, final ResponseListener<Object> listener) {
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
try {
Cast.CastApi.setMute(mApiClient, isMute);
Util.postSuccess(listener, null);
} catch (Exception e) {
Util.postError(listener, new ServiceCommandError(0, "setting mute status failed", null));
}
}
};
runCommand(connectionListener);
}
@Override
public void getMute(final MuteListener listener) {
Util.postSuccess(listener, currentMuteStatus);
}
@Override
public ServiceSubscription<VolumeListener> subscribeVolume(VolumeListener listener) {
URLServiceSubscription<VolumeListener> request = new URLServiceSubscription<VolumeListener>(this, CAST_SERVICE_VOLUME_SUBSCRIPTION_NAME, null, null);
request.addListener(listener);
addSubscription(request);
return request;
}
@Override
public ServiceSubscription<MuteListener> subscribeMute(MuteListener listener) {
URLServiceSubscription<MuteListener> request = new URLServiceSubscription<MuteListener>(this, CAST_SERVICE_MUTE_SUBSCRIPTION_NAME, null, null);
request.addListener(listener);
addSubscription(request);
return request;
}
@Override
protected void updateCapabilities() {
List<String> capabilities = new ArrayList<String>();
Collections.addAll(capabilities, MediaPlayer.Capabilities);
Collections.addAll(capabilities, VolumeControl.Capabilities);
capabilities.add(Play);
capabilities.add(Pause);
capabilities.add(Stop);
capabilities.add(Duration);
capabilities.add(Seek);
capabilities.add(Position);
capabilities.add(PlayState);
capabilities.add(Subtitles_Vtt);
capabilities.add(PlayState_Subscribe);
capabilities.add(WebAppLauncher.Launch);
capabilities.add(Message_Send);
capabilities.add(Message_Receive);
capabilities.add(Message_Send_JSON);
capabilities.add(Message_Receive_JSON);
capabilities.add(WebAppLauncher.Connect);
capabilities.add(WebAppLauncher.Disconnect);
capabilities.add(WebAppLauncher.Join);
capabilities.add(WebAppLauncher.Close);
setCapabilities(capabilities);
}
private class CastListener extends Cast.Listener {
@Override
public void onApplicationDisconnected(int statusCode) {
Log.d(Util.T, "Cast.Listener.onApplicationDisconnected: " + statusCode);
if (currentAppId == null)
return;
CastWebAppSession webAppSession = sessions.get(currentAppId);
if (webAppSession == null)
return;
webAppSession.handleAppClose();
currentAppId = null;
}
@Override
public void onApplicationStatusChanged() {
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
if (mApiClient != null) {
ApplicationMetadata applicationMetadata = Cast.CastApi.getApplicationMetadata(mApiClient);
if (applicationMetadata != null)
currentAppId = applicationMetadata.getApplicationId();
}
}
};
runCommand(connectionListener);
}
@Override
public void onVolumeChanged() {
ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void onConnected() {
try {
currentVolumeLevel = (float) Cast.CastApi.getVolume(mApiClient);
currentMuteStatus = Cast.CastApi.isMute(mApiClient);
} catch (Exception e) {
e.printStackTrace();
}
if (subscriptions.size() > 0) {
for (URLServiceSubscription<?> subscription: subscriptions) {
if (subscription.getTarget().equals(CAST_SERVICE_VOLUME_SUBSCRIPTION_NAME)) {
for (int i = 0; i < subscription.getListeners().size(); i++) {
@SuppressWarnings("unchecked")
ResponseListener<Object> listener = (ResponseListener<Object>) subscription.getListeners().get(i);
Util.postSuccess(listener, currentVolumeLevel);
}
}
else if (subscription.getTarget().equals(CAST_SERVICE_MUTE_SUBSCRIPTION_NAME)) {
for (int i = 0; i < subscription.getListeners().size(); i++) {
@SuppressWarnings("unchecked")
ResponseListener<Object> listener = (ResponseListener<Object>) subscription.getListeners().get(i);
Util.postSuccess(listener, currentMuteStatus);
}
}
}
}
}
};
runCommand(connectionListener);
}
}
private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks {
@Override
public void onConnectionSuspended(final int cause) {
Log.d(Util.T, "ConnectionCallbacks.onConnectionSuspended");
mWaitingForReconnect = true;
detachMediaPlayer();
}
@Override
public void onConnected(Bundle connectionHint) {
Log.d(Util.T, "ConnectionCallbacks.onConnected, wasWaitingForReconnect: " + mWaitingForReconnect);
attachMediaPlayer();
if (mApiClient != null) {
Cast.CastApi.joinApplication(mApiClient).setResultCallback(new ResultCallback<Cast.ApplicationConnectionResult>() {
@Override
public void onResult(ApplicationConnectionResult result) {
if (result.getStatus().isSuccess()) {
// TODO: Maybe there is better way to check current cast device is showing backdrop, but for now, if chromecast is showing backdrop, then requestStatus would never response.
if (result.getApplicationMetadata() != null &&
result.getApplicationMetadata().getName() != null &&
!result.getApplicationMetadata().getName().equals("Backdrop") &&
mMediaPlayer != null && mApiClient != null) {
mMediaPlayer.requestStatus(mApiClient).setResultCallback(
new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
joinFinished();
}
});
}
else {
joinFinished();
}
}
else {
joinFinished();
}
}
});
}
}
private void joinFinished() {
if (mWaitingForReconnect) {
mWaitingForReconnect = false;
}
else {
connected = true;
reportConnected(true);
}
if (!commandQueue.isEmpty()) {
for (ConnectionListener listener : commandQueue) {
listener.onConnected();
commandQueue.remove(listener);
}
}
}
}
private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener {
@Override
public void onConnectionFailed(final ConnectionResult result) {
Log.d(Util.T, "ConnectionFailedListener.onConnectionFailed " + (result != null ? result: ""));
detachMediaPlayer();
connected = false;
mWaitingForReconnect = false;
mApiClient = null;
Util.runOnUI(new Runnable() {
@Override
public void run() {
if (listener != null) {
ServiceCommandError error = new ServiceCommandError(result.getErrorCode(), "Failed to connect to Google Cast device", result);
listener.onConnectionFailure(CastService.this, error);
}
}
});
}
}
private class ApplicationConnectionResultCallback implements
ResultCallback<Cast.ApplicationConnectionResult> {
LaunchWebAppListener listener;
public ApplicationConnectionResultCallback(LaunchWebAppListener listener) {
this.listener = listener;
}
@Override
public void onResult(ApplicationConnectionResult result) {
Status status = result.getStatus();
if (status.isSuccess()) {
ApplicationMetadata applicationMetadata = result.getApplicationMetadata();
currentAppId = applicationMetadata.getApplicationId();
LaunchSession launchSession = LaunchSession.launchSessionForAppId(applicationMetadata.getApplicationId());
launchSession.setAppName(applicationMetadata.getName());
launchSession.setSessionId(result.getSessionId());
launchSession.setSessionType(LaunchSessionType.WebApp);
launchSession.setService(CastService.this);
CastWebAppSession webAppSession = new CastWebAppSession(launchSession, CastService.this);
webAppSession.setMetadata(applicationMetadata);
sessions.put(applicationMetadata.getApplicationId(), webAppSession);
if (listener != null) {
listener.onSuccess(webAppSession);
}
launchingAppId = null;
}
else {
if (listener != null) {
listener.onFailure(new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status));
}
}
}
}
@Override
public void getPlayState(PlayStateListener listener) {
if (mMediaPlayer != null && mMediaPlayer.getMediaStatus() != null) {
PlayStateStatus status = PlayStateStatus.convertPlayerStateToPlayStateStatus(mMediaPlayer.getMediaStatus().getPlayerState());
Util.postSuccess(listener, status);
}
else {
Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null));
}
}
public GoogleApiClient getApiClient() {
return mApiClient;
}
//////////////////////////////////////////////////
// Device Service Methods
//////////////////////////////////////////////////
@Override
public boolean isConnectable() {
return true;
}
@Override
public boolean isConnected() {
return connected;
}
@Override
public ServiceSubscription<PlayStateListener> subscribePlayState(PlayStateListener listener) {
URLServiceSubscription<PlayStateListener> request = new URLServiceSubscription<PlayStateListener>(this, PLAY_STATE, null, null);
request.addListener(listener);
addSubscription(request);
return request;
}
private void addSubscription(URLServiceSubscription<?> subscription) {
subscriptions.add(subscription);
}
@Override
public void unsubscribe(URLServiceSubscription<?> subscription) {
subscriptions.remove(subscription);
}
public List<URLServiceSubscription<?>> getSubscriptions() {
return subscriptions;
}
public void setSubscriptions(List<URLServiceSubscription<?>> subscriptions) {
this.subscriptions = subscriptions;
}
private void runCommand(ConnectionListener connectionListener) {
if (mApiClient != null && mApiClient.isConnected()) {
connectionListener.onConnected();
}
else {
connect();
commandQueue.add(connectionListener);
}
}
}