From eb869ef20b466d9890283c574d2f584e8bb2638d Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Wed, 25 Apr 2018 19:25:14 -0400 Subject: [PATCH] Cast MVP to start videos --- extern/GmsApi | 2 +- play-services-core/build.gradle | 14 ++ .../src/main/AndroidManifest.xml | 7 +- .../framework/internal/CastContextImpl.java | 52 +++++- .../internal/CastDynamiteModuleImpl.java | 27 +-- .../framework/internal/CastSessionImpl.java | 56 ++++++ .../internal/DiscoveryManagerImpl.java | 1 - .../internal/MediaRouterCallbackImpl.java | 25 ++- .../cast/framework/internal/SessionImpl.java | 158 ++++++++++++++++- .../internal/SessionManagerImpl.java | 159 +++++++++++++++++- .../gms/cast/CastDeviceControllerImpl.java | 151 +++++++++++++++++ .../gms/cast/CastDeviceControllerService.java | 50 ++++++ .../gms/cast/CastMediaRouteController.java | 96 +++++++++++ .../gms/cast/CastMediaRouteProvider.java | 117 +++++++++---- proguard.flags | 6 + settings.gradle | 2 +- 16 files changed, 846 insertions(+), 77 deletions(-) create mode 100644 play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerImpl.java create mode 100644 play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerService.java create mode 100644 play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteController.java diff --git a/extern/GmsApi b/extern/GmsApi index a811b2f6..5af21c0f 160000 --- a/extern/GmsApi +++ b/extern/GmsApi @@ -1 +1 @@ -Subproject commit a811b2f64572a10e34a0fd9a0eac189735849f5b +Subproject commit 5af21c0ff2777e61caba5d111991cbb165167793 diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 6444474d..d4bfe525 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -16,6 +16,10 @@ apply plugin: 'com.android.application' +repositories { + mavenLocal() +} + dependencies { implementation "com.android.support:support-v4:$supportLibraryVersion" implementation "com.android.support:appcompat-v7:$supportLibraryVersion" @@ -23,6 +27,12 @@ dependencies { implementation "com.takisoft.fix:preference-v7:$supportLibraryVersion.0" implementation "de.hdodenhof:circleimageview:1.3.0" implementation "com.squareup.wire:wire-runtime:1.6.1" + implementation "su.litvak.chromecast:api-v2:0.10.3-SNAPSHOT" + + // Specified manually due to + // https://github.com/vitalidze/chromecast-java-api-v2/issues/91 + api "org.slf4j:slf4j-api:1.7.25" + api "uk.uuid.slf4j:slf4j-android:1.7.25-1" implementation project(':microg-ui-tools') implementation project(':play-services-api') @@ -101,6 +111,10 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + packagingOptions { + exclude 'META-INF/ASL2.0' + } } if (file('user.gradle').exists()) { diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 9b30d7aa..042e4c33 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -595,6 +595,12 @@ + + + + + + @@ -603,7 +609,6 @@ - diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java index bb0ecda0..e0307a2e 100644 --- a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java @@ -18,15 +18,20 @@ package com.google.android.gms.cast.framework.internal; import android.content.Context; import android.os.Bundle; +import android.os.IBinder; import android.os.RemoteException; import android.support.v7.media.MediaControlIntent; import android.support.v7.media.MediaRouteSelector; +import android.support.v7.media.MediaRouter; import android.util.Log; +import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.framework.CastOptions; +import com.google.android.gms.cast.framework.IAppVisibilityListener; import com.google.android.gms.cast.framework.ICastContext; import com.google.android.gms.cast.framework.IDiscoveryManager; import com.google.android.gms.cast.framework.ISessionManager; +import com.google.android.gms.cast.framework.ISessionProvider; import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.dynamic.ObjectWrapper; @@ -41,22 +46,39 @@ public class CastContextImpl extends ICastContext.Stub { private Context context; private CastOptions options; private IMediaRouter router; - private Map map; + private Map sessionProviders; + public ISessionProvider defaultSessionProvider; private MediaRouteSelector mergedSelector; - public CastContextImpl(IObjectWrapper context, CastOptions options, IMediaRouter router, Map map) { - Log.d(TAG, "Creating new cast context"); + public CastContextImpl(IObjectWrapper context, CastOptions options, IMediaRouter router, Map sessionProviders) throws RemoteException { this.context = (Context) ObjectWrapper.unwrap(context); this.options = options; this.router = router; - this.map = map; + this.sessionProviders = sessionProviders; + + String receiverApplicationId = options.getReceiverApplicationId(); + String defaultCategory = CastMediaControlIntent.categoryForCast(receiverApplicationId); + + this.defaultSessionProvider = ISessionProvider.Stub.asInterface(this.sessionProviders.get(defaultCategory)); // TODO: This should incorporate passed options this.mergedSelector = new MediaRouteSelector.Builder() .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) .build(); + + // TODO: Find a home for this once the rest of the implementation + // becomes more clear. Uncomment this to enable discovery of devices. + // Note that the scan currently isn't ever disabled as part of the + // lifecycle, so we don't want to ship with this. + /* + Bundle selectorBundle = this.mergedSelector.asBundle(); + + router.clearCallbacks(); + router.registerMediaRouterCallbackImpl(selectorBundle, new MediaRouterCallbackImpl(this)); + router.addCallback(selectorBundle, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); + */ } @Override @@ -64,6 +86,16 @@ public class CastContextImpl extends ICastContext.Stub { return this.mergedSelector.asBundle(); } + @Override + public void addVisibilityChangeListener(IAppVisibilityListener listener) { + Log.d(TAG, "unimplemented Method: addVisibilityChangeListener"); + } + + @Override + public void removeVisibilityChangeListener(IAppVisibilityListener listener) { + Log.d(TAG, "unimplemented Method: removeVisibilityChangeListener"); + } + @Override public boolean isApplicationVisible() throws RemoteException { Log.d(TAG, "unimplemented Method: isApplicationVisible"); @@ -71,9 +103,9 @@ public class CastContextImpl extends ICastContext.Stub { } @Override - public ISessionManager getSessionManagerImpl() throws RemoteException { + public SessionManagerImpl getSessionManagerImpl() { if (this.sessionManager == null) { - this.sessionManager = new SessionManagerImpl(); + this.sessionManager = new SessionManagerImpl(this); } return this.sessionManager; } @@ -103,8 +135,8 @@ public class CastContextImpl extends ICastContext.Stub { } @Override - public void unknown(String s1, Map m1) throws RemoteException { - Log.d(TAG, "unimplemented Method: unknown"); + public void setReceiverApplicationId(String receiverApplicationId, Map sessionProvidersByCategory) throws RemoteException { + Log.d(TAG, "unimplemented Method: setReceiverApplicationId"); } public Context getContext() { @@ -119,6 +151,10 @@ public class CastContextImpl extends ICastContext.Stub { return this.mergedSelector; } + public CastOptions getOptions() { + return this.options; + } + @Override public IObjectWrapper getWrappedThis() throws RemoteException { return ObjectWrapper.wrap(this); diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastDynamiteModuleImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastDynamiteModuleImpl.java index cd40264f..3bc3bd81 100644 --- a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastDynamiteModuleImpl.java +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastDynamiteModuleImpl.java @@ -18,6 +18,7 @@ package com.google.android.gms.cast.framework.internal; import android.content.Context; import android.os.Bundle; +import android.os.IBinder; import android.os.RemoteException; import android.support.v7.media.MediaRouter; import android.util.Log; @@ -45,34 +46,18 @@ public class CastDynamiteModuleImpl extends ICastDynamiteModule.Stub { private static final String TAG = CastDynamiteModuleImpl.class.getSimpleName(); @Override - public ICastContext newCastContextImpl(IObjectWrapper context, CastOptions options, IMediaRouter router, Map map) throws RemoteException { - CastContextImpl castContextImpl = new CastContextImpl(context, options, router, map); - - // TODO: Find a home for this once the rest of the implementation - // becomes more clear. Uncomment this to enable discovery of devices. - // Note that the scan currently isn't ever disabled as part of the - // lifecycle, so we don't want to ship with this. - /* - Bundle selectorBundle = castContextImpl.getMergedSelector().asBundle(); - - router.clearCallbacks(); - router.registerMediaRouterCallbackImpl(selectorBundle, new MediaRouterCallbackImpl()); - router.addCallback(selectorBundle, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); - */ - - return castContextImpl; + public ICastContext newCastContextImpl(IObjectWrapper context, CastOptions options, IMediaRouter router, Map sessionProviders) throws RemoteException { + return new CastContextImpl(context, options, router, sessionProviders); } @Override - public ISession newSessionImpl(String s1, String s2, ISessionProxy proxy) throws RemoteException { - Log.d(TAG, "unimplemented Method: newSessionImpl"); - return new SessionImpl(); + public ISession newSessionImpl(String category, String sessionId, ISessionProxy proxy) throws RemoteException { + return new SessionImpl(category, sessionId, proxy); } @Override public ICastSession newCastSessionImpl(CastOptions options, IObjectWrapper session, ICastConnectionController controller) throws RemoteException { - Log.d(TAG, "unimplemented Method: newCastSessionImpl"); - return new CastSessionImpl(); + return new CastSessionImpl(options, session, controller); } @Override diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastSessionImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastSessionImpl.java index 993e5da1..cc63b949 100644 --- a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastSessionImpl.java +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastSessionImpl.java @@ -18,6 +18,62 @@ package com.google.android.gms.cast.framework.internal; import com.google.android.gms.cast.framework.ICastSession; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.cast.ApplicationMetadata; +import com.google.android.gms.cast.framework.CastOptions; +import com.google.android.gms.cast.framework.ICastConnectionController; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.dynamic.IObjectWrapper; +import com.google.android.gms.dynamic.ObjectWrapper; + public class CastSessionImpl extends ICastSession.Stub { private static final String TAG = CastSessionImpl.class.getSimpleName(); + private CastOptions options; + private SessionImpl session; + private ICastConnectionController controller; + + public CastSessionImpl(CastOptions options, IObjectWrapper session, ICastConnectionController controller) throws RemoteException { + this.options = options; + this.session = (SessionImpl) ObjectWrapper.unwrap(session); + this.controller = controller; + + this.session.setCastSession(this); + } + + public void launchApplication() throws RemoteException { + this.controller.launchApplication(this.options.getReceiverApplicationId(), this.options.getLaunchOptions()); + } + + @Override + public void onConnected(Bundle routeInfoExtra) throws RemoteException { + this.controller.launchApplication(this.options.getReceiverApplicationId(), this.options.getLaunchOptions()); + } + + @Override + public void onConnectionSuspended(int reason) { + Log.d(TAG, "unimplemented Method: onConnectionSuspended"); + } + + @Override + public void onConnectionFailed(Status status) { + Log.d(TAG, "unimplemented Method: onConnectionFailed"); + } + + @Override + public void onApplicationConnectionSuccess(ApplicationMetadata applicationMetadata, String applicationStatus, String sessionId, boolean wasLaunched) { + this.session.onApplicationConnectionSuccess(applicationMetadata, applicationStatus, sessionId, wasLaunched); + } + + @Override + public void onApplicationConnectionFailure(int statusCode) { + this.session.onApplicationConnectionFailure(statusCode); + } + + @Override + public void disconnectFromDevice(boolean boolean1, int int1) { + Log.d(TAG, "unimplemented Method: disconnectFromDevice"); + } } diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/DiscoveryManagerImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/DiscoveryManagerImpl.java index 53c67b8b..b0e7b51c 100644 --- a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/DiscoveryManagerImpl.java +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/DiscoveryManagerImpl.java @@ -36,7 +36,6 @@ public class DiscoveryManagerImpl extends IDiscoveryManager.Stub { private Set discoveryManagerListeners = new HashSet(); public DiscoveryManagerImpl(CastContextImpl castContextImpl) { - Log.d(TAG, "Creating new discovery manager"); this.castContextImpl = castContextImpl; } diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java index 59ea5742..8bf933f3 100644 --- a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java @@ -16,12 +16,27 @@ package com.google.android.gms.cast.framework.internal; +import android.content.Intent; import android.os.Bundle; +import android.os.RemoteException; import android.util.Log; +import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.framework.ISession; +import com.google.android.gms.dynamic.IObjectWrapper; +import com.google.android.gms.dynamic.ObjectWrapper; + +import android.support.v7.media.MediaControlIntent; + public class MediaRouterCallbackImpl extends IMediaRouterCallback.Stub { private static final String TAG = MediaRouterCallbackImpl.class.getSimpleName(); + private CastContextImpl castContext; + + public MediaRouterCallbackImpl(CastContextImpl castContext) { + this.castContext = castContext; + } + @Override public void onRouteAdded(String routeId, Bundle extras) { Log.d(TAG, "unimplemented Method: onRouteAdded"); @@ -35,8 +50,14 @@ public class MediaRouterCallbackImpl extends IMediaRouterCallback.Stub { Log.d(TAG, "unimplemented Method: onRouteRemoved"); } @Override - public void onRouteSelected(String routeId, Bundle extras) { - Log.d(TAG, "unimplemented Method: onRouteSelected"); + public void onRouteSelected(String routeId, Bundle extras) throws RemoteException { + CastDevice castDevice = CastDevice.getFromBundle(extras); + + SessionImpl session = (SessionImpl) ObjectWrapper.unwrap(this.castContext.defaultSessionProvider.getSession(null)); + Bundle routeInfoExtras = this.castContext.getRouter().getRouteInfoExtrasById(routeId); + if (routeInfoExtras != null) { + session.start(this.castContext, castDevice, routeId, routeInfoExtras); + } } @Override public void unknown(String routeId, Bundle extras) { diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionImpl.java index 5d0185c3..954405d8 100644 --- a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionImpl.java +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionImpl.java @@ -16,24 +16,136 @@ package com.google.android.gms.cast.framework.internal; +import android.os.Bundle; +import android.os.RemoteException; import android.util.Log; + +import com.google.android.gms.cast.ApplicationMetadata; +import com.google.android.gms.cast.CastDevice; import com.google.android.gms.cast.framework.ISession; +import com.google.android.gms.cast.framework.ISessionProxy; import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.dynamic.ObjectWrapper; public class SessionImpl extends ISession.Stub { private static final String TAG = SessionImpl.class.getSimpleName(); + private String category; + private String sessionId; + private ISessionProxy proxy; + + private CastSessionImpl castSession; + + private CastContextImpl castContext; + private CastDevice castDevice; + private Bundle routeInfoExtra; + + private boolean mIsConnecting = false; + private boolean mIsConnected = false; + private String routeId = null; + + public SessionImpl(String category, String sessionId, ISessionProxy proxy) { + this.category = category; + this.sessionId = sessionId; + this.proxy = proxy; + } + + public void start(CastContextImpl castContext, CastDevice castDevice, String routeId, Bundle routeInfoExtra) throws RemoteException { + this.castContext = castContext; + this.castDevice = castDevice; + this.routeInfoExtra = routeInfoExtra; + this.routeId = routeId; + + this.mIsConnecting = true; + this.mIsConnected = false; + this.castContext.getSessionManagerImpl().onSessionStarting(this); + this.proxy.start(routeInfoExtra); + } + + public void onApplicationConnectionSuccess(ApplicationMetadata applicationMetadata, String applicationStatus, String sessionId, boolean wasLaunched) { + this.mIsConnecting = false; + this.mIsConnected = true; + this.castContext.getSessionManagerImpl().onSessionStarted(this, sessionId); + try { + this.castContext.getRouter().selectRouteById(this.getRouteId()); + } catch (RemoteException ex) { + Log.e(TAG, "Error calling selectRouteById: " + ex.getMessage()); + } + } + + public void onApplicationConnectionFailure(int statusCode) { + this.mIsConnecting = false; + this.mIsConnected = false; + this.routeId = null; + this.castContext = null; + this.castDevice = null; + this.routeInfoExtra = null; + this.castContext.getSessionManagerImpl().onSessionStartFailed(this, statusCode); + try { + this.castContext.getRouter().selectDefaultRoute(); + } catch (RemoteException ex) { + Log.e(TAG, "Error calling selectDefaultRoute: " + ex.getMessage()); + } + } + + public void onRouteSelected(Bundle extras) { + } + + public CastSessionImpl getCastSession() { + return this.castSession; + } + + public void setCastSession(CastSessionImpl castSession) { + this.castSession = castSession; + } + + public ISessionProxy getSessionProxy() { + return this.proxy; + } + + public IObjectWrapper getWrappedSession() throws RemoteException { + if (this.proxy == null) { + return ObjectWrapper.wrap(null); + } + return this.proxy.getWrappedSession(); + } + @Override - public void notifySessionEnded(int error) { - Log.d(TAG, "unimplemented Method: notifySessionEnded"); + public String getCategory() { + return this.category; + } + + @Override + public String getSessionId() { + return this.sessionId; + } + + @Override + public String getRouteId() { + return this.routeId; } @Override public boolean isConnected() { - Log.d(TAG, "unimplemented Method: isConnected"); - return true; + return this.mIsConnected; + } + + @Override + public boolean isConnecting() { + return this.mIsConnecting; + } + + @Override + public boolean isDisconnecting() { + Log.d(TAG, "unimplemented Method: isDisconnecting"); + return false; + } + + @Override + public boolean isDisconnected() { + Log.d(TAG, "unimplemented Method: isDisconnected"); + return false; } @Override @@ -43,7 +155,43 @@ public class SessionImpl extends ISession.Stub { } @Override - public IObjectWrapper getWrappedThis() { + public boolean isSuspended() { + Log.d(TAG, "unimplemented Method: isSuspended"); + return false; + } + + @Override + public void notifySessionStarted(String sessionId) { + Log.d(TAG, "unimplemented Method: notifySessionStarted"); + } + + @Override + public void notifyFailedToStartSession(int error) { + Log.d(TAG, "unimplemented Method: notifyFailedToStartSession"); + } + + @Override + public void notifySessionEnded(int error) { + Log.d(TAG, "unimplemented Method: notifySessionEnded"); + } + + @Override + public void notifySessionResumed(boolean wasSuspended) { + Log.d(TAG, "unimplemented Method: notifySessionResumed"); + } + + @Override + public void notifyFailedToResumeSession(int error) { + Log.d(TAG, "unimplemented Method: notifyFailedToResumeSession"); + } + + @Override + public void notifySessionSuspended(int reason) { + Log.d(TAG, "unimplemented Method: notifySessionSuspended"); + } + + @Override + public IObjectWrapper getWrappedObject() { return ObjectWrapper.wrap(this); } } diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionManagerImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionManagerImpl.java index 6b0bc005..d10f8b21 100644 --- a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionManagerImpl.java +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionManagerImpl.java @@ -16,12 +16,16 @@ package com.google.android.gms.cast.framework.internal; +import android.os.Bundle; import android.os.RemoteException; import android.util.Log; +import com.google.android.gms.cast.framework.CastState; import com.google.android.gms.cast.framework.ICastStateListener; +import com.google.android.gms.cast.framework.ISession; import com.google.android.gms.cast.framework.ISessionManager; import com.google.android.gms.cast.framework.ISessionManagerListener; +import com.google.android.gms.cast.framework.internal.CastContextImpl; import com.google.android.gms.cast.framework.internal.SessionImpl; import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.dynamic.ObjectWrapper; @@ -29,18 +33,33 @@ import com.google.android.gms.dynamic.ObjectWrapper; import java.util.Set; import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; + public class SessionManagerImpl extends ISessionManager.Stub { private static final String TAG = SessionManagerImpl.class.getSimpleName(); - private Set sessionManagerListeners = new HashSet(); - private Set castStateListeners = new HashSet(); + private CastContextImpl castContext; + + private Set sessionManagerListeners = new HashSet(); + private Set castStateListeners = new HashSet(); + + private Map routeSessions = new HashMap(); private SessionImpl currentSession; + private int castState = CastState.NO_DEVICES_AVAILABLE; + + public SessionManagerImpl(CastContextImpl castContext) { + this.castContext = castContext; + } + @Override public IObjectWrapper getWrappedCurrentSession() throws RemoteException { - Log.d(TAG, "unimplemented Method: getWrappedCurrentSession"); - return ObjectWrapper.wrap(this.currentSession); + if (this.currentSession == null) { + return ObjectWrapper.wrap(null); + } + return this.currentSession.getWrappedSession(); } @Override @@ -76,4 +95,136 @@ public class SessionManagerImpl extends ISessionManager.Stub { public IObjectWrapper getWrappedThis() throws RemoteException { return ObjectWrapper.wrap(this); } + + @Override + public int getCastState() { + return this.castState; + } + + @Override + public void startSession(Bundle params) { + Log.d(TAG, "unimplemented Method: startSession"); + String routeId = params.getString("CAST_INTENT_TO_CAST_ROUTE_ID_KEY"); + String sessionId = params.getString("CAST_INTENT_TO_CAST_SESSION_ID_KEY"); + } + + public void onRouteSelected(String routeId, Bundle extras) { + Log.d(TAG, "unimplemented Method: onRouteSelected: " + routeId); + } + + private void setCastState(int castState) { + this.castState = castState; + this.onCastStateChanged(); + } + + public void onCastStateChanged() { + for (ICastStateListener listener : this.castStateListeners) { + try { + listener.onCastStateChanged(this.castState); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onCastStateChanged: " + e.getMessage()); + } + } + } + + public void onSessionStarting(SessionImpl session) { + this.setCastState(CastState.CONNECTING); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionStarting(session.getSessionProxy().getWrappedSession()); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionStarting: " + e.getMessage()); + } + } + } + + public void onSessionStartFailed(SessionImpl session, int error) { + this.currentSession = null; + this.setCastState(CastState.NOT_CONNECTED); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionStartFailed(session.getSessionProxy().getWrappedSession(), error); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionStartFailed: " + e.getMessage()); + } + } + } + + public void onSessionStarted(SessionImpl session, String sessionId) { + this.currentSession = session; + this.setCastState(CastState.CONNECTED); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionStarted(session.getSessionProxy().getWrappedSession(), sessionId); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionStarted: " + e.getMessage()); + } + } + } + + public void onSessionResumed(SessionImpl session, boolean wasSuspended) { + this.setCastState(CastState.CONNECTED); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionResumed(session.getSessionProxy().getWrappedSession(), wasSuspended); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionResumed: " + e.getMessage()); + } + } + } + + public void onSessionEnding(SessionImpl session) { + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionEnding(session.getSessionProxy().getWrappedSession()); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionEnding: " + e.getMessage()); + } + } + } + + public void onSessionEnded(SessionImpl session, int error) { + this.currentSession = null; + this.setCastState(CastState.NOT_CONNECTED); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionEnded(session.getSessionProxy().getWrappedSession(), error); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionEnded: " + e.getMessage()); + } + } + } + + public void onSessionResuming(SessionImpl session, String sessionId) { + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionResuming(session.getSessionProxy().getWrappedSession(), sessionId); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionResuming: " + e.getMessage()); + } + } + } + + public void onSessionResumeFailed(SessionImpl session, int error) { + this.currentSession = null; + this.setCastState(CastState.NOT_CONNECTED); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionResumeFailed(session.getSessionProxy().getWrappedSession(), error); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionResumeFailed: " + e.getMessage()); + } + } + } + + public void onSessionSuspended(SessionImpl session, int reason) { + this.setCastState(CastState.NOT_CONNECTED); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionSuspended(session.getSessionProxy().getWrappedSession(), reason); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionSuspended: " + e.getMessage()); + } + } + } } diff --git a/play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerImpl.java b/play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerImpl.java new file mode 100644 index 00000000..f163b868 --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerImpl.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * 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 org.microg.gms.cast; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; +import android.os.RemoteException; +import android.util.Base64; +import android.util.Log; + +import com.google.android.gms.cast.ApplicationMetadata; +import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.JoinOptions; +import com.google.android.gms.cast.LaunchOptions; +import com.google.android.gms.cast.internal.ICastDeviceController; +import com.google.android.gms.cast.internal.ICastDeviceControllerListener; +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.images.WebImage; +import com.google.android.gms.common.internal.BinderWrapper; +import com.google.android.gms.common.internal.GetServiceRequest; + +import su.litvak.chromecast.api.v2.Application; +import su.litvak.chromecast.api.v2.ChromeCast; +import su.litvak.chromecast.api.v2.Namespace; + +public class CastDeviceControllerImpl extends ICastDeviceController.Stub { + private static final String TAG = "GmsCastDeviceControllerImpl"; + + private Context context; + private String packageName; + private CastDevice castDevice; + boolean notificationEnabled; + long castFlags; + ICastDeviceControllerListener listener; + + ChromeCast chromecast; + + String sessionId = null; + + public CastDeviceControllerImpl(Context context, String packageName, Bundle extras) { + this.context = context; + this.packageName = packageName; + + extras.setClassLoader(BinderWrapper.class.getClassLoader()); + this.castDevice = CastDevice.getFromBundle(extras); + this.notificationEnabled = extras.getBoolean("com.google.android.gms.cast.EXTRA_CAST_FRAMEWORK_NOTIFICATION_ENABLED"); + this.castFlags = extras.getLong("com.google.android.gms.cast.EXTRA_CAST_FLAGS"); + BinderWrapper listenerWrapper = (BinderWrapper)extras.get("listener"); + this.listener = ICastDeviceControllerListener.Stub.asInterface(listenerWrapper.binder); + + this.chromecast = new ChromeCast(this.castDevice.getAddress()); + } + + @Override + public void disconnect() { + Log.d(TAG, "unimplemented Method: disconnect"); + this.sessionId = null; + } + + @Override + public void sendMessage(String namespace, String message, long requestId) { + String response = null; + try { + response = this.chromecast.sendRawRequest(namespace, message, requestId); + } catch (IOException e) { + Log.w(TAG, "Error sending cast message: " + e.getMessage()); + return; + } + try { + this.listener.onSendMessageSuccess(response, requestId); + } catch (RemoteException ex) { + Log.e(TAG, "Error calling onSendMessageSuccess: " + ex.getMessage()); + } + } + + @Override + public void stopApplication(String sessionId) { + Log.d(TAG, "unimplemented Method: stopApplication"); + this.sessionId = null; + } + + @Override + public void registerNamespace(String namespace) { + Log.d(TAG, "unimplemented Method: registerNamespace"); + } + + @Override + public void unregisterNamespace(String namespace) { + Log.d(TAG, "unimplemented Method: unregisterNamespace"); + } + + @Override + public void launchApplication(String applicationId, LaunchOptions launchOptions) { + Application app = null; + try { + app = this.chromecast.launchApp(applicationId); + } catch (IOException e) { + Log.w(TAG, "Error launching cast application: " + e.getMessage()); + try { + this.listener.onApplicationConnectionFailure(CommonStatusCodes.NETWORK_ERROR); + } catch (RemoteException ex) { + Log.e(TAG, "Error calling onApplicationConnectionFailure: " + ex.getMessage()); + } + return; + } + this.sessionId = app.sessionId; + + ApplicationMetadata metadata = new ApplicationMetadata(); + metadata.applicationId = applicationId; + metadata.name = app.name; + Log.d(TAG, "unimplemented: ApplicationMetadata.images"); + metadata.images = new ArrayList(); + metadata.namespaces = new ArrayList(); + Log.d(TAG, "unimplemented: ApplicationMetadata.senderAppLaunchUri"); + for(Namespace namespace : app.namespaces) { + metadata.namespaces.add(namespace.name); + } + metadata.senderAppIdentifier = this.context.getPackageName(); + try { + this.listener.onApplicationConnectionSuccess(metadata, app.statusText, app.sessionId, true); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onApplicationConnectionSuccess: " + e.getMessage()); + } + } + + @Override + public void joinApplication(String applicationId, String sessionId, JoinOptions joinOptions) { + Log.d(TAG, "unimplemented Method: joinApplication"); + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerService.java b/play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerService.java new file mode 100644 index 00000000..d494a012 --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerService.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * 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 org.microg.gms.cast; + +import android.os.IBinder; +import android.os.RemoteException; +import android.os.Parcel; +import android.util.ArrayMap; +import android.util.Log; + +import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.internal.ICastDeviceControllerListener; +import com.google.android.gms.common.internal.GetServiceRequest; +import com.google.android.gms.common.internal.BinderWrapper; +import com.google.android.gms.common.internal.IGmsCallbacks; + +import org.microg.gms.BaseService; +import org.microg.gms.common.GmsService; + +import su.litvak.chromecast.api.v2.ChromeCast; +import su.litvak.chromecast.api.v2.ChromeCasts; +import su.litvak.chromecast.api.v2.Status; +import su.litvak.chromecast.api.v2.ChromeCastsListener; + +public class CastDeviceControllerService extends BaseService { + private static final String TAG = CastDeviceControllerService.class.getSimpleName(); + + public CastDeviceControllerService() { + super("GmsCastDeviceControllerSvc", GmsService.CAST); + } + + @Override + public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException { + callback.onPostInitComplete(0, new CastDeviceControllerImpl(this, request.packageName, request.extras), null); + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteController.java b/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteController.java new file mode 100644 index 00000000..de79d9b4 --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteController.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * 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 org.microg.gms.cast; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.Bundle; +import android.os.AsyncTask; +import android.os.Handler; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouteDescriptor; +import android.support.v7.media.MediaRouteDiscoveryRequest; +import android.support.v7.media.MediaRouteProvider; +import android.support.v7.media.MediaRouteProviderDescriptor; +import android.support.v7.media.MediaRouter; +import android.util.Log; + +import com.google.android.gms.common.images.WebImage; +import com.google.android.gms.cast.CastDevice; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Inet4Address; +import java.net.UnknownHostException; +import java.io.IOException; +import java.lang.Thread; +import java.lang.Runnable; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; + +import su.litvak.chromecast.api.v2.ChromeCast; +import su.litvak.chromecast.api.v2.ChromeCasts; +import su.litvak.chromecast.api.v2.Status; +import su.litvak.chromecast.api.v2.ChromeCastsListener; + +public class CastMediaRouteController extends MediaRouteProvider.RouteController { + private static final String TAG = CastMediaRouteController.class.getSimpleName(); + + private CastMediaRouteProvider provider; + private String routeId; + private ChromeCast chromecast; + + public CastMediaRouteController(CastMediaRouteProvider provider, String routeId, ChromeCast chromecast) { + super(); + + this.provider = provider; + this.routeId = routeId; + this.chromecast = chromecast; + } + + public boolean onControlRequest(Intent intent, MediaRouter.ControlRequestCallback callback) { + Log.d(TAG, "unimplemented Method: onControlRequest: " + this.routeId); + return false; + } + + public void onRelease() { + Log.d(TAG, "unimplemented Method: onRelease: " + this.routeId); + } + + public void onSelect() { + Log.d(TAG, "unimplemented Method: onSelect: " + this.routeId); + } + + public void onSetVolume(int volume) { + Log.d(TAG, "unimplemented Method: onSetVolume: " + this.routeId); + } + + public void onUnselect() { + Log.d(TAG, "unimplemented Method: onUnselect: " + this.routeId); + } + + public void onUnselect(int reason) { + Log.d(TAG, "unimplemented Method: onUnselect: " + this.routeId); + } + + public void onUpdateVolume(int delta) { + Log.d(TAG, "unimplemented Method: onUpdateVolume: " + this.routeId); + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java b/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java index efd99955..e7b104eb 100644 --- a/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java +++ b/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java @@ -20,6 +20,8 @@ import android.content.Context; import android.content.IntentFilter; import android.net.Uri; import android.os.Bundle; +import android.os.AsyncTask; +import android.os.Handler; import android.support.v7.media.MediaControlIntent; import android.support.v7.media.MediaRouteDescriptor; import android.support.v7.media.MediaRouteDiscoveryRequest; @@ -31,19 +33,68 @@ import android.util.Log; import com.google.android.gms.common.images.WebImage; import com.google.android.gms.cast.CastDevice; -import java.util.ArrayList; import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.Inet4Address; +import java.net.UnknownHostException; +import java.io.IOException; +import java.lang.Thread; +import java.lang.Runnable; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; + +import su.litvak.chromecast.api.v2.ChromeCast; +import su.litvak.chromecast.api.v2.ChromeCasts; +import su.litvak.chromecast.api.v2.Status; +import su.litvak.chromecast.api.v2.ChromeCastsListener; public class CastMediaRouteProvider extends MediaRouteProvider { private static final String TAG = CastMediaRouteProvider.class.getSimpleName(); + private Map chromecasts = new HashMap(); + public CastMediaRouteProvider(Context context) { super(context); - Log.d(TAG, "unimplemented Method: CastMediaRouteProvider"); - // Uncomment this to create the mock device - // publishRoutes(); + // TODO: Uncomment this to manually discover a chromecast on the local + // network. Discovery not yet implemented. + /* + InetAddress addr = null; + try { + addr = InetAddress.getByName("192.168.1.11"); + } catch (UnknownHostException e) { + Log.d(TAG, "Chromecast status exception getting host: " + e.getMessage()); + return; + } + onChromeCastDiscovered(addr); + */ + } + + private void onChromeCastDiscovered(InetAddress address) { + ChromeCast chromecast = new ChromeCast(address.getHostAddress()); + + new Thread(new Runnable() { + public void run() { + Status status = null; + try { + status = chromecast.getStatus(); + } catch (IOException e) { + Log.e(TAG, "Exception getting chromecast status: " + e.getMessage()); + return; + } + + Handler mainHandler = new Handler(CastMediaRouteProvider.this.getContext().getMainLooper()); + mainHandler.post(new Runnable() { + @Override + public void run() { + String routeId = address.getHostAddress(); + CastMediaRouteProvider.this.chromecasts.put(routeId, chromecast); + publishRoutes(); + } + }); + } + }).start(); } /** @@ -67,37 +118,37 @@ public class CastMediaRouteProvider extends MediaRouteProvider { @Override public RouteController onCreateRouteController(String routeId) { - Log.d(TAG, "unimplemented Method: onCreateRouteController"); - return null; + ChromeCast chromecast = this.chromecasts.get(routeId); + return new CastMediaRouteController(this, routeId, chromecast); } - /** - * TODO: Currently this method simply publishes a single cast route for - * testing. - */ private void publishRoutes() { - Log.d(TAG, "unimplemented Method: publishRoutes"); - Bundle extras = new Bundle(); - CastDevice castDevice = new CastDevice("abc123"); - castDevice.putInBundle(extras); - MediaRouteDescriptor routeDescriptor1 = new MediaRouteDescriptor.Builder( - "abc123", - "Rotue Friendly Name") - .setDescription("Chromecast") - .addControlFilters(CONTROL_FILTERS) - .setDeviceType(MediaRouter.RouteInfo.DEVICE_TYPE_TV) - .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) - .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED) - .setVolumeMax(20) - .setVolume(0) - .setEnabled(true) - .setExtras(extras) - .setConnectionState(MediaRouter.RouteInfo.CONNECTION_STATE_DISCONNECTED) - .build(); - MediaRouteProviderDescriptor providerDescriptor = - new MediaRouteProviderDescriptor.Builder() - .addRoute(routeDescriptor1) - .build(); - this.setDescriptor(providerDescriptor); + MediaRouteProviderDescriptor.Builder builder = new MediaRouteProviderDescriptor.Builder(); + for(Map.Entry entry : this.chromecasts.entrySet()) { + String routeId = entry.getKey(); + ChromeCast chromecast = entry.getValue(); + Bundle extras = new Bundle(); + CastDevice castDevice = new CastDevice( + routeId, + chromecast.getAddress() + ); + castDevice.putInBundle(extras); + builder.addRoute(new MediaRouteDescriptor.Builder( + routeId, + routeId) + .setDescription("Chromecast") + .addControlFilters(CONTROL_FILTERS) + .setDeviceType(MediaRouter.RouteInfo.DEVICE_TYPE_TV) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED) + .setVolumeMax(20) + .setVolume(0) + .setEnabled(true) + .setExtras(extras) + .setConnectionState(MediaRouter.RouteInfo.CONNECTION_STATE_DISCONNECTED) + .build()); + } + + this.setDescriptor(builder.build()); } } diff --git a/proguard.flags b/proguard.flags index 198869bb..6be46b9c 100644 --- a/proguard.flags +++ b/proguard.flags @@ -14,6 +14,7 @@ -dontwarn org.oscim.tiling.source.OkHttpEngine$OkHttpFactory -dontwarn com.caverock.androidsvg.** -dontwarn org.slf4j.** +-dontwarn org.codehaus.jackson.** # Disable ProGuard Notes, they won't help here -dontnote @@ -46,3 +47,8 @@ -keep public class com.squareup.wire.Message -keep public class * extends com.squareup.wire.Message -keep public class * extends com.squareup.wire.Message$Builder { public (...); } + +# Proguard configuration for Jackson 1.x +-keepclassmembers class * { + @org.codehaus.jackson.annotate.* *; +} diff --git a/settings.gradle b/settings.gradle index ff311f8f..eae21e8a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ -include ':wearable-lib' +include ':wearable-lib', ':safe-parcel' include ':unifiednlp-api' include ':unifiednlp-base'