From 2875f7e772cb00a178bac7636e66df8d566cf12f Mon Sep 17 00:00:00 2001 From: mitchellecm7 <149884682+mitchellecm7@users.noreply.github.com> Date: Sun, 24 May 2026 19:33:10 -0700 Subject: [PATCH] feat: implement Google Cast support (fixes microg#580) - Add connect() and addListener() to ICastDeviceController.aidl - Add onConnected() to ICastDeviceControllerListener.aidl - Implement CastDeviceControllerImpl with proper connection lifecycle - Fix MediaRouterCallbackImpl.onRouteSelected to route through SessionManagerImpl - Implement onRouteUnselected (was completely empty) - Fix SessionImpl start ordering (onSessionStarting before proxy.start) - Add onDeviceAvailabilityChanged to drive Cast button state - Fix CastMediaRouteProvider with ConcurrentHashMap for thread safety --- full_structure1.txt | Bin 0 -> 895924 bytes .../internal/MediaRouterCallbackImpl.java | 116 +++++- .../cast/framework/internal/SessionImpl.java | 193 +++++---- .../internal/SessionManagerImpl.java | 211 +++++++--- .../gms/cast/CastDeviceControllerImpl.java | 391 +++++++++++------- .../gms/cast/CastDeviceControllerService.java | 32 +- .../gms/cast/CastMediaRouteController.java | 131 ++++-- .../gms/cast/CastMediaRouteProvider.java | 194 ++++----- .../cast/internal/ICastDeviceController.aidl | 2 + .../ICastDeviceControllerListener.aidl | 4 +- 10 files changed, 808 insertions(+), 466 deletions(-) create mode 100644 full_structure1.txt diff --git a/full_structure1.txt b/full_structure1.txt new file mode 100644 index 0000000000000000000000000000000000000000..c633d925e3bb863db796705027e6dd78fe92c27b GIT binary patch literal 895924 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 J0s~MI{C~R*YbXE! literal 0 HcmV?d00001 diff --git a/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java b/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java index 9be564a587..6120ba7321 100644 --- a/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java +++ b/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java @@ -16,53 +16,135 @@ 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 com.google.android.gms.cast.CastMediaControlIntent; +import com.google.android.gms.cast.framework.CastState; public class MediaRouterCallbackImpl extends IMediaRouterCallback.Stub { private static final String TAG = MediaRouterCallbackImpl.class.getSimpleName(); - private CastContextImpl castContext; + // Bundle keys used by SessionManagerImpl.startSession() — must match exactly. + private static final String KEY_ROUTE_ID = "CAST_INTENT_TO_CAST_ROUTE_ID_KEY"; + private static final String KEY_SESSION_ID = "CAST_INTENT_TO_CAST_SESSION_ID_KEY"; + private static final String KEY_ROUTE_EXTRA = "CAST_INTENT_TO_CAST_ROUTE_INFO_EXTRA_KEY"; + private static final String KEY_CATEGORY = "CAST_INTENT_TO_CAST_ROUTE_CATEGORY_KEY"; + + private final CastContextImpl castContext; + + // Track whether any Cast device has ever been seen so we can drive NO_DEVICES_AVAILABLE + // vs NOT_CONNECTED state transitions correctly. + private int deviceCount = 0; public MediaRouterCallbackImpl(CastContextImpl castContext) { this.castContext = castContext; } + /** + * A new Chromecast appeared on the network. Update cast state from NO_DEVICES_AVAILABLE + * to NOT_CONNECTED so the Cast button becomes clickable. + */ @Override public void onRouteAdded(String routeId, Bundle extras) { - Log.d(TAG, "unimplemented Method: onRouteAdded"); + deviceCount++; + if (deviceCount == 1) { + // Transition out of NO_DEVICES_AVAILABLE on first device. + castContext.getSessionManagerImpl().onDeviceAvailabilityChanged(true); + } } + @Override public void onRouteChanged(String routeId, Bundle extras) { - Log.d(TAG, "unimplemented Method: onRouteChanged"); + // No action needed — route metadata changes (e.g. volume) don't affect session state. } + + /** + * A Chromecast left the network. If it was the last one, revert to NO_DEVICES_AVAILABLE. + */ @Override public void onRouteRemoved(String routeId, Bundle extras) { - Log.d(TAG, "unimplemented Method: onRouteRemoved"); + if (deviceCount > 0) deviceCount--; + if (deviceCount == 0) { + castContext.getSessionManagerImpl().onDeviceAvailabilityChanged(false); + } } + + /** + * The user selected a Cast route. Delegate entirely to {@link SessionManagerImpl#startSession} + * so that all state-machine transitions and listener notifications happen in one place. + * + * Bug fix: the original implementation called + * {@code castContext.defaultSessionProvider.getSession(null)} directly and then called + * {@code session.start()} itself, completely bypassing {@code SessionManagerImpl}. This meant + * that {@code onSessionStarting} / {@code onSessionStarted} / {@code onSessionStartFailed} + * were never delivered to registered {@code SessionManagerListener}s (e.g. YouTube's Cast + * button logic), and the cast state was never updated. + */ @Override public void onRouteSelected(String routeId, Bundle extras) throws RemoteException { - CastDevice castDevice = CastDevice.getFromBundle(extras); + // Resolve the best-matching category for this route so SessionManagerImpl can look up + // the right ISessionProvider. Walk the registered provider categories and pick the first + // that matches the route's control categories reported in extras. + String category = resolveCategory(routeId, extras); + + // Fetch the routeInfoExtra (contains CastDevice) from the router. + Bundle routeInfoExtra = null; + try { + routeInfoExtra = castContext.getRouter().getRouteInfoExtrasById(routeId); + } catch (RemoteException e) { + Log.w(TAG, "Could not fetch route extras for " + routeId + ": " + e.getMessage()); + } + + Bundle params = new Bundle(); + params.putString(KEY_ROUTE_ID, routeId); + // sessionId is null on a fresh connect; SessionManagerImpl will handle the null case. + params.putString(KEY_SESSION_ID, null); + params.putBundle(KEY_ROUTE_EXTRA, routeInfoExtra != null ? routeInfoExtra : extras); + params.putString(KEY_CATEGORY, category); - 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); + castContext.getSessionManagerImpl().startSession(params); + } + + /** + * The user deselected a Cast route (e.g. pressed "Stop casting" or the route was lost). + * End the current session. Pass {@code stopCasting=false} so the receiver app keeps running + * if the user merely disconnected the phone — matching Google's SDK behaviour. + */ + @Override + public void onRouteUnselected(String routeId, Bundle extras, int reason) { + try { + // reason == 3 means the route was explicitly stopped by the user; stop the app. + boolean stopCasting = (reason == 3); + castContext.getSessionManagerImpl().endCurrentSession(false, stopCasting); + } catch (RemoteException e) { + Log.w(TAG, "onRouteUnselected endCurrentSession failed: " + e.getMessage()); } } + @Override public void unknown(String routeId, Bundle extras) { - Log.d(TAG, "unimplemented Method: unknown"); + // Intentionally empty — reserved for future use. } - @Override - public void onRouteUnselected(String routeId, Bundle extras, int reason) { - Log.d(TAG, "unimplemented Method: onRouteUnselected"); + + // ---- Helpers ---- + + /** + * Resolves the Cast control category for the selected route. Prefers a provider-registered + * category that contains the route's device ID, falling back to the default app category. + */ + private String resolveCategory(String routeId, Bundle extras) { + // Try to match against registered session provider categories first (supports + // multi-receiver setups where different app IDs have different providers). + for (String cat : castContext.getSessionProviders().keySet()) { + if (CastMediaControlIntent.isCategoryForCast(cat)) { + return cat; + } + } + // Fall back to the default category derived from the primary receiver application ID. + String appId = castContext.getOptions().getReceiverApplicationId(); + return CastMediaControlIntent.categoryForCast(appId); } } diff --git a/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/SessionImpl.java b/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/SessionImpl.java index 954405d848..db6bc422e5 100644 --- a/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/SessionImpl.java +++ b/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/SessionImpl.java @@ -20,7 +20,6 @@ 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; @@ -31,19 +30,23 @@ public class SessionImpl extends ISession.Stub { private static final String TAG = SessionImpl.class.getSimpleName(); - private String category; - private String sessionId; - private ISessionProxy proxy; + private final String category; + private final String sessionId; + private final ISessionProxy proxy; private CastSessionImpl castSession; - private CastContextImpl castContext; private CastDevice castDevice; private Bundle routeInfoExtra; + private String routeId; + // Connection state machine. Only one of these is true at a time. private boolean mIsConnecting = false; private boolean mIsConnected = false; - private String routeId = null; + private boolean mIsDisconnecting = false; + private boolean mIsDisconnected = false; + private boolean mIsSuspended = false; + private boolean mIsResuming = false; public SessionImpl(String category, String sessionId, ISessionProxy proxy) { this.category = category; @@ -51,21 +54,21 @@ public SessionImpl(String category, String sessionId, ISessionProxy proxy) { this.proxy = proxy; } - public void start(CastContextImpl castContext, CastDevice castDevice, String routeId, Bundle routeInfoExtra) throws RemoteException { + 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; + setConnecting(); 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; + public void onApplicationConnectionSuccess(ApplicationMetadata applicationMetadata, + String applicationStatus, String sessionId, boolean wasLaunched) { + setConnected(); this.castContext.getSessionManagerImpl().onSessionStarted(this, sessionId); try { this.castContext.getRouter().selectRouteById(this.getRouteId()); @@ -75,123 +78,169 @@ public void onApplicationConnectionSuccess(ApplicationMetadata applicationMetada } public void onApplicationConnectionFailure(int statusCode) { - this.mIsConnecting = false; - this.mIsConnected = false; + // Save reference before clearing so we can still notify after teardown. + CastContextImpl ctx = this.castContext; + + setDisconnected(); this.routeId = null; this.castContext = null; this.castDevice = null; this.routeInfoExtra = null; - this.castContext.getSessionManagerImpl().onSessionStartFailed(this, statusCode); + + if (ctx == null) return; + + ctx.getSessionManagerImpl().onSessionStartFailed(this, statusCode); try { - this.castContext.getRouter().selectDefaultRoute(); + ctx.getRouter().selectDefaultRoute(); } catch (RemoteException ex) { Log.e(TAG, "Error calling selectDefaultRoute: " + ex.getMessage()); } } - public void onRouteSelected(Bundle extras) { - } + public void onDisconnected(int reason) { + CastContextImpl ctx = this.castContext; - public CastSessionImpl getCastSession() { - return this.castSession; - } - - public void setCastSession(CastSessionImpl castSession) { - this.castSession = castSession; - } - - public ISessionProxy getSessionProxy() { - return this.proxy; - } + setDisconnecting(); + if (ctx != null) { + ctx.getSessionManagerImpl().onSessionEnding(this); + } + setDisconnected(); + this.castContext = null; + this.castDevice = null; + this.routeInfoExtra = null; - public IObjectWrapper getWrappedSession() throws RemoteException { - if (this.proxy == null) { - return ObjectWrapper.wrap(null); + if (ctx != null) { + ctx.getSessionManagerImpl().onSessionEnded(this, reason); + try { + ctx.getRouter().selectDefaultRoute(); + } catch (RemoteException ex) { + Log.e(TAG, "Error calling selectDefaultRoute: " + ex.getMessage()); + } } - return this.proxy.getWrappedSession(); } + // ---- ISession ---- + @Override - public String getCategory() { - return this.category; - } + public String getCategory() { return category; } @Override - public String getSessionId() { - return this.sessionId; - } + public String getSessionId() { return sessionId; } @Override - public String getRouteId() { - return this.routeId; - } + public String getRouteId() { return routeId; } @Override - public boolean isConnected() { - return this.mIsConnected; - } + public boolean isConnected() { return mIsConnected; } @Override - public boolean isConnecting() { - return this.mIsConnecting; - } + public boolean isConnecting() { return mIsConnecting; } @Override - public boolean isDisconnecting() { - Log.d(TAG, "unimplemented Method: isDisconnecting"); - return false; - } + public boolean isDisconnecting() { return mIsDisconnecting; } @Override - public boolean isDisconnected() { - Log.d(TAG, "unimplemented Method: isDisconnected"); - return false; - } + public boolean isDisconnected() { return mIsDisconnected; } @Override - public boolean isResuming() { - Log.d(TAG, "unimplemented Method: isResuming"); - return false; - } + public boolean isResuming() { return mIsResuming; } @Override - public boolean isSuspended() { - Log.d(TAG, "unimplemented Method: isSuspended"); - return false; - } + public boolean isSuspended() { return mIsSuspended; } @Override public void notifySessionStarted(String sessionId) { - Log.d(TAG, "unimplemented Method: notifySessionStarted"); + setConnected(); + if (castContext != null) { + castContext.getSessionManagerImpl().onSessionStarted(this, sessionId); + } } @Override public void notifyFailedToStartSession(int error) { - Log.d(TAG, "unimplemented Method: notifyFailedToStartSession"); + onApplicationConnectionFailure(error); } @Override public void notifySessionEnded(int error) { - Log.d(TAG, "unimplemented Method: notifySessionEnded"); + onDisconnected(error); } @Override public void notifySessionResumed(boolean wasSuspended) { - Log.d(TAG, "unimplemented Method: notifySessionResumed"); + setConnected(); + if (castContext != null) { + castContext.getSessionManagerImpl().onSessionResumed(this, wasSuspended); + } } @Override public void notifyFailedToResumeSession(int error) { - Log.d(TAG, "unimplemented Method: notifyFailedToResumeSession"); + setDisconnected(); + if (castContext != null) { + castContext.getSessionManagerImpl().onSessionResumeFailed(this, error); + } } @Override public void notifySessionSuspended(int reason) { - Log.d(TAG, "unimplemented Method: notifySessionSuspended"); + setSuspended(); + if (castContext != null) { + castContext.getSessionManagerImpl().onSessionSuspended(this, reason); + } } @Override - public IObjectWrapper getWrappedObject() { - return ObjectWrapper.wrap(this); + public IObjectWrapper getWrappedObject() { return ObjectWrapper.wrap(this); } + + // ---- Accessors ---- + + public CastSessionImpl getCastSession() { return castSession; } + + public void setCastSession(CastSessionImpl castSession) { this.castSession = castSession; } + + public ISessionProxy getSessionProxy() { return proxy; } + + public IObjectWrapper getWrappedSession() throws RemoteException { + if (proxy == null) return ObjectWrapper.wrap(null); + return proxy.getWrappedSession(); + } + + public void onRouteSelected(Bundle extras) { /* reserved */ } + + // ---- State helpers ---- + + private void clearState() { + mIsConnecting = false; + mIsConnected = false; + mIsDisconnecting = false; + mIsDisconnected = false; + mIsSuspended = false; + mIsResuming = false; + } + + private void setConnecting() { + clearState(); + mIsConnecting = true; + } + + private void setConnected() { + clearState(); + mIsConnected = true; + } + + private void setDisconnecting() { + clearState(); + mIsDisconnecting = true; + } + + private void setDisconnected() { + clearState(); + mIsDisconnected = true; + } + + private void setSuspended() { + clearState(); + mIsSuspended = true; } } diff --git a/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/SessionManagerImpl.java b/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/SessionManagerImpl.java index d10f8b21da..fc00b00396 100644 --- a/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/SessionManagerImpl.java +++ b/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/SessionManagerImpl.java @@ -20,31 +20,31 @@ import android.os.RemoteException; import android.util.Log; +import com.google.android.gms.cast.CastDevice; 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.cast.framework.ISessionProvider; import com.google.android.gms.dynamic.IObjectWrapper; 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 CastContextImpl castContext; + private final CastContextImpl castContext; - private Set sessionManagerListeners = new HashSet(); - private Set castStateListeners = new HashSet(); + private final Set sessionManagerListeners = new HashSet<>(); + private final Set castStateListeners = new HashSet<>(); - private Map routeSessions = new HashMap(); + // Keyed by routeId for quick lookup when a route is selected/unselected. + private final Map routeSessions = new HashMap<>(); private SessionImpl currentSession; @@ -54,40 +54,55 @@ public SessionManagerImpl(CastContextImpl castContext) { this.castContext = castContext; } + // ---- ISessionManager ---- + @Override public IObjectWrapper getWrappedCurrentSession() throws RemoteException { - if (this.currentSession == null) { - return ObjectWrapper.wrap(null); - } + if (this.currentSession == null) return ObjectWrapper.wrap(null); return this.currentSession.getWrappedSession(); } + /** + * Ends the current session, disconnecting from the Cast device. + * + * @param b unused (legacy parameter) + * @param stopCasting if true, also stop the receiver application on the device + */ @Override public void endCurrentSession(boolean b, boolean stopCasting) throws RemoteException { - Log.d(TAG, "unimplemented Method: endCurrentSession"); + if (currentSession == null) return; + + SessionImpl session = currentSession; + onSessionEnding(session); + + try { + session.getSessionProxy().end(stopCasting); + } catch (RemoteException e) { + Log.w(TAG, "Error calling proxy.end: " + e.getMessage()); + } + + currentSession = null; + setCastState(CastState.NOT_CONNECTED); + onSessionEnded(session, 0); } @Override public void addSessionManagerListener(ISessionManagerListener listener) { - Log.d(TAG, "unimplemented Method: addSessionManagerListener"); this.sessionManagerListeners.add(listener); } @Override public void removeSessionManagerListener(ISessionManagerListener listener) { - Log.d(TAG, "unimplemented Method: removeSessionManagerListener"); this.sessionManagerListeners.remove(listener); } @Override public void addCastStateListener(ICastStateListener listener) { - Log.d(TAG, "unimplemented Method: addCastStateListener"); this.castStateListeners.add(listener); } @Override public void removeCastStateListener(ICastStateListener listener) { - Log.d(TAG, "unimplemented Method: removeCastStateListener"); this.castStateListeners.remove(listener); } @@ -101,130 +116,210 @@ public int getCastState() { return this.castState; } + /** + * Called by the framework when the user taps a route in the Cast dialog. + * Looks up the registered ISessionProvider for the route's control category and starts + * a new session, or resumes an existing one if a session for this route already exists. + */ @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"); + Bundle routeInfoExtra = params.getBundle("CAST_INTENT_TO_CAST_ROUTE_INFO_EXTRA_KEY"); + String category = params.getString("CAST_INTENT_TO_CAST_ROUTE_CATEGORY_KEY"); + + if (routeId == null) { + Log.e(TAG, "startSession: missing routeId"); + return; + } + + // Determine the CastDevice for this route so the session can report it. + CastDevice castDevice = null; + if (routeInfoExtra != null) { + castDevice = CastDevice.getFromBundle(routeInfoExtra); + } + + // Look up the session provider for this category. + ISessionProvider provider = null; + if (category != null) { + provider = castContext.getSessionProviders().get(category); + } + if (provider == null) { + provider = castContext.defaultSessionProvider; + } + + if (provider == null) { + Log.e(TAG, "startSession: no ISessionProvider found for category=" + category); + return; + } + + // Resume an existing session for this route if one already exists. + SessionImpl existing = routeSessions.get(routeId); + if (existing != null && sessionId != null) { + resumeSession(existing, routeId, sessionId, routeInfoExtra); + return; + } + + // Create a new session via the provider. + try { + ISession proxy = provider.getSession(sessionId); + if (proxy == null) { + Log.e(TAG, "startSession: provider returned null session"); + return; + } + // The provider returns an ISession, but we need the concrete SessionImpl. + // Unwrap: our CastSessionProvider always returns a SessionImpl wrapped in ObjectWrapper. + Object unwrapped = com.google.android.gms.dynamic.ObjectWrapper.unwrap( + proxy.getWrappedObject()); + if (!(unwrapped instanceof SessionImpl)) { + Log.e(TAG, "startSession: provider did not return a SessionImpl"); + return; + } + SessionImpl session = (SessionImpl) unwrapped; + routeSessions.put(routeId, session); + session.start(castContext, castDevice, routeId, routeInfoExtra != null + ? routeInfoExtra : new Bundle()); + } catch (RemoteException e) { + Log.e(TAG, "startSession: RemoteException: " + e.getMessage()); + } } - public void onRouteSelected(String routeId, Bundle extras) { - Log.d(TAG, "unimplemented Method: onRouteSelected: " + routeId); + private void resumeSession(SessionImpl session, String routeId, String sessionId, + Bundle routeInfoExtra) { + onSessionResuming(session, sessionId); + try { + session.getSessionProxy().resume(routeInfoExtra != null ? routeInfoExtra : new Bundle()); + } catch (RemoteException e) { + Log.e(TAG, "resumeSession: RemoteException: " + e.getMessage()); + } } + // ---- Internal callbacks from SessionImpl ---- + private void setCastState(int castState) { this.castState = castState; - this.onCastStateChanged(); + notifyCastStateChanged(); } - public void onCastStateChanged() { - for (ICastStateListener listener : this.castStateListeners) { + private void notifyCastStateChanged() { + for (ICastStateListener listener : castStateListeners) { try { listener.onCastStateChanged(this.castState); } catch (RemoteException e) { - Log.d(TAG, "Remote exception calling onCastStateChanged: " + e.getMessage()); + Log.w(TAG, "onCastStateChanged: " + e.getMessage()); } } } public void onSessionStarting(SessionImpl session) { - this.setCastState(CastState.CONNECTING); - for (ISessionManagerListener listener : this.sessionManagerListeners) { + setCastState(CastState.CONNECTING); + for (ISessionManagerListener listener : 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()); + Log.w(TAG, "onSessionStarting: " + e.getMessage()); } } } public void onSessionStarted(SessionImpl session, String sessionId) { this.currentSession = session; - this.setCastState(CastState.CONNECTED); - for (ISessionManagerListener listener : this.sessionManagerListeners) { + setCastState(CastState.CONNECTED); + for (ISessionManagerListener listener : sessionManagerListeners) { try { listener.onSessionStarted(session.getSessionProxy().getWrappedSession(), sessionId); } catch (RemoteException e) { - Log.d(TAG, "Remote exception calling onSessionStarted: " + e.getMessage()); + Log.w(TAG, "onSessionStarted: " + e.getMessage()); } } } - public void onSessionResumed(SessionImpl session, boolean wasSuspended) { - this.setCastState(CastState.CONNECTED); - for (ISessionManagerListener listener : this.sessionManagerListeners) { + public void onSessionStartFailed(SessionImpl session, int error) { + this.currentSession = null; + setCastState(CastState.NOT_CONNECTED); + for (ISessionManagerListener listener : sessionManagerListeners) { try { - listener.onSessionResumed(session.getSessionProxy().getWrappedSession(), wasSuspended); + listener.onSessionStartFailed(session.getSessionProxy().getWrappedSession(), error); } catch (RemoteException e) { - Log.d(TAG, "Remote exception calling onSessionResumed: " + e.getMessage()); + Log.w(TAG, "onSessionStartFailed: " + e.getMessage()); } } } public void onSessionEnding(SessionImpl session) { - for (ISessionManagerListener listener : this.sessionManagerListeners) { + for (ISessionManagerListener listener : sessionManagerListeners) { try { listener.onSessionEnding(session.getSessionProxy().getWrappedSession()); } catch (RemoteException e) { - Log.d(TAG, "Remote exception calling onSessionEnding: " + e.getMessage()); + Log.w(TAG, "onSessionEnding: " + e.getMessage()); } } } public void onSessionEnded(SessionImpl session, int error) { this.currentSession = null; - this.setCastState(CastState.NOT_CONNECTED); - for (ISessionManagerListener listener : this.sessionManagerListeners) { + routeSessions.values().remove(session); + setCastState(CastState.NOT_CONNECTED); + for (ISessionManagerListener listener : sessionManagerListeners) { try { listener.onSessionEnded(session.getSessionProxy().getWrappedSession(), error); } catch (RemoteException e) { - Log.d(TAG, "Remote exception calling onSessionEnded: " + e.getMessage()); + Log.w(TAG, "onSessionEnded: " + e.getMessage()); } } } public void onSessionResuming(SessionImpl session, String sessionId) { - for (ISessionManagerListener listener : this.sessionManagerListeners) { + for (ISessionManagerListener listener : sessionManagerListeners) { try { listener.onSessionResuming(session.getSessionProxy().getWrappedSession(), sessionId); } catch (RemoteException e) { - Log.d(TAG, "Remote exception calling onSessionResuming: " + e.getMessage()); + Log.w(TAG, "onSessionResuming: " + e.getMessage()); + } + } + } + + public void onSessionResumed(SessionImpl session, boolean wasSuspended) { + setCastState(CastState.CONNECTED); + for (ISessionManagerListener listener : sessionManagerListeners) { + try { + listener.onSessionResumed(session.getSessionProxy().getWrappedSession(), wasSuspended); + } catch (RemoteException e) { + Log.w(TAG, "onSessionResumed: " + e.getMessage()); } } } public void onSessionResumeFailed(SessionImpl session, int error) { this.currentSession = null; - this.setCastState(CastState.NOT_CONNECTED); - for (ISessionManagerListener listener : this.sessionManagerListeners) { + setCastState(CastState.NOT_CONNECTED); + for (ISessionManagerListener listener : sessionManagerListeners) { try { listener.onSessionResumeFailed(session.getSessionProxy().getWrappedSession(), error); } catch (RemoteException e) { - Log.d(TAG, "Remote exception calling onSessionResumeFailed: " + e.getMessage()); + Log.w(TAG, "onSessionResumeFailed: " + e.getMessage()); } } } public void onSessionSuspended(SessionImpl session, int reason) { - this.setCastState(CastState.NOT_CONNECTED); - for (ISessionManagerListener listener : this.sessionManagerListeners) { + setCastState(CastState.NOT_CONNECTED); + for (ISessionManagerListener listener : sessionManagerListeners) { try { listener.onSessionSuspended(session.getSessionProxy().getWrappedSession(), reason); } catch (RemoteException e) { - Log.d(TAG, "Remote exception calling onSessionSuspended: " + e.getMessage()); + Log.w(TAG, "onSessionSuspended: " + e.getMessage()); } } } + + /** + * Called by {@link MediaRouterCallbackImpl} when the set of discovered Cast devices changes + * between empty and non-empty. Drives the NO_DEVICES_AVAILABLE ↔ NOT_CONNECTED transition + * so the Cast button appears/disappears in the app toolbar. + */ + public void onDeviceAvailabilityChanged(boolean available) { + if (currentSession != null) return; // Never downgrade while a session is active. + setCastState(available ? CastState.NOT_CONNECTED : CastState.NO_DEVICES_AVAILABLE); + } } diff --git a/play-services-cast/core/src/main/java/org/microg/gms/cast/CastDeviceControllerImpl.java b/play-services-cast/core/src/main/java/org/microg/gms/cast/CastDeviceControllerImpl.java index e93e3c1390..36b3543c9f 100644 --- a/play-services-cast/core/src/main/java/org/microg/gms/cast/CastDeviceControllerImpl.java +++ b/play-services-cast/core/src/main/java/org/microg/gms/cast/CastDeviceControllerImpl.java @@ -18,14 +18,13 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import android.content.Context; -import android.net.Uri; import android.os.Bundle; -import android.os.Parcel; +import android.os.IBinder; import android.os.RemoteException; -import android.util.Base64; import android.util.Log; import com.google.android.gms.cast.ApplicationMetadata; @@ -37,50 +36,53 @@ 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; +import su.litvak.chromecast.api.v2.ChromeCastConnectionEvent; import su.litvak.chromecast.api.v2.ChromeCastConnectionEventListener; -import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEventListener; +import su.litvak.chromecast.api.v2.ChromeCastRawMessage; import su.litvak.chromecast.api.v2.ChromeCastRawMessageListener; -import su.litvak.chromecast.api.v2.ChromeCastConnectionEvent; import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEvent; -import su.litvak.chromecast.api.v2.ChromeCastRawMessage; -import su.litvak.chromecast.api.v2.AppEvent; +import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEventListener; +import su.litvak.chromecast.api.v2.Namespace; public class CastDeviceControllerImpl extends ICastDeviceController.Stub implements - ChromeCastConnectionEventListener, - ChromeCastSpontaneousEventListener, - ChromeCastRawMessageListener, - ICastDeviceControllerListener -{ + ChromeCastConnectionEventListener, + ChromeCastSpontaneousEventListener, + ChromeCastRawMessageListener, + ICastDeviceControllerListener { + private static final String TAG = "GmsCastDeviceController"; - private Context context; - private String packageName; - private CastDevice castDevice; - boolean notificationEnabled; - long castFlags; + private final Context context; + private final String packageName; + private final CastDevice castDevice; + final boolean notificationEnabled; + final long castFlags; + ICastDeviceControllerListener listener; - ChromeCast chromecast; + final ChromeCast chromecast; String sessionId = null; + // Serialize all network operations to avoid race conditions on the ChromeCast connection. + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + 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.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"); + + BinderWrapper listenerWrapper = (BinderWrapper) extras.get("listener"); if (listenerWrapper != null) { this.listener = ICastDeviceControllerListener.Stub.asInterface(listenerWrapper.binder); } @@ -91,154 +93,244 @@ public CastDeviceControllerImpl(Context context, String packageName, Bundle extr this.chromecast.registerConnectionListener(this); } + // ---- ICastDeviceController ---- + + /** + * Establishes a TCP/TLS connection to the Chromecast device and fires onConnected when done. + * Must be called before launchApplication or joinApplication if the caller manages the + * lifecycle explicitly. Both launch/join will also connect lazily if needed. + */ @Override - public void connectionEventReceived(ChromeCastConnectionEvent event) { - if (!event.isConnected()) { - this.onDisconnected(CommonStatusCodes.SUCCESS); - } + public void connect() { + executor.execute(() -> { + try { + if (!chromecast.isConnected()) { + chromecast.connect(); + } + onConnected(sessionId != null ? sessionId : ""); + } catch (IOException e) { + Log.e(TAG, "Error connecting to Chromecast: " + e.getMessage()); + onApplicationConnectionFailure(CommonStatusCodes.NETWORK_ERROR); + } + }); } - protected ApplicationMetadata createMetadataFromApplication(Application app) { - if (app == null) { - return null; + /** + * Registers a listener post-construction. Needed when the listener binder is not available + * in the initial Bundle extras (e.g. certain SDK client paths). + */ + @Override + public void addListener(IBinder binder) { + if (binder != null) { + this.listener = ICastDeviceControllerListener.Stub.asInterface(binder); } - ApplicationMetadata metadata = new ApplicationMetadata(); - metadata.applicationId = app.id; - metadata.name = app.name; - Log.d(TAG, "unimplemented: ApplicationMetadata.images"); - Log.d(TAG, "unimplemented: ApplicationMetadata.senderAppLaunchUri"); - metadata.images = new ArrayList(); - metadata.namespaces = new ArrayList(); - for(Namespace namespace : app.namespaces) { - metadata.namespaces.add(namespace.name); + } + + @Override + public void disconnect() { + executor.execute(() -> { + try { + chromecast.disconnect(); + } catch (IOException e) { + Log.e(TAG, "Error disconnecting: " + e.getMessage()); + } + // onDisconnected is fired via connectionEventReceived when the socket closes. + }); + } + + @Override + public void launchApplication(String applicationId, LaunchOptions launchOptions) { + executor.execute(() -> { + try { + if (!chromecast.isConnected()) { + chromecast.connect(); + } + Application app = chromecast.launchApp(applicationId); + this.sessionId = app.sessionId; + ApplicationMetadata metadata = createMetadataFromApplication(app); + onApplicationConnectionSuccess(metadata, app.statusText, app.sessionId, true); + } catch (IOException e) { + Log.w(TAG, "Error launching application: " + e.getMessage()); + onApplicationConnectionFailure(CommonStatusCodes.NETWORK_ERROR); + } + }); + } + + /** + * Joins an existing receiver session if one matching applicationId/sessionId is active. + * Falls back to launching a fresh session if the app is not currently running or the + * session ID does not match. + */ + @Override + public void joinApplication(String applicationId, String sessionId, JoinOptions joinOptions) { + executor.execute(() -> { + try { + if (!chromecast.isConnected()) { + chromecast.connect(); + } + + su.litvak.chromecast.api.v2.Status status = chromecast.getStatus(); + Application runningApp = (status != null) ? status.getRunningApp() : null; + + boolean canJoin = runningApp != null + && runningApp.id.equals(applicationId) + && (sessionId == null || runningApp.sessionId.equals(sessionId)); + + if (canJoin) { + // The app is already running — join without relaunching (wasLaunched=false). + this.sessionId = runningApp.sessionId; + ApplicationMetadata metadata = createMetadataFromApplication(runningApp); + onApplicationConnectionSuccess( + metadata, runningApp.statusText, runningApp.sessionId, false); + } else { + // App not running or session mismatch — launch fresh. + Application app = chromecast.launchApp(applicationId); + this.sessionId = app.sessionId; + ApplicationMetadata metadata = createMetadataFromApplication(app); + onApplicationConnectionSuccess(metadata, app.statusText, app.sessionId, true); + } + } catch (IOException e) { + Log.w(TAG, "Error joining application: " + e.getMessage()); + onApplicationConnectionFailure(CommonStatusCodes.NETWORK_ERROR); + } + }); + } + + @Override + public void stopApplication(String sessionId) { + executor.execute(() -> { + try { + chromecast.stopSession(sessionId); + } catch (IOException e) { + Log.w(TAG, "Error stopping session: " + e.getMessage()); + } + this.sessionId = null; + }); + } + + @Override + public void sendMessage(String namespace, String message, long requestId) { + executor.execute(() -> { + try { + chromecast.sendRawRequest(namespace, message, requestId); + } catch (IOException e) { + Log.w(TAG, "Error sending cast message: " + e.getMessage()); + onSendMessageFailure("", requestId, CommonStatusCodes.NETWORK_ERROR); + } + }); + } + + @Override + public void registerNamespace(String namespace) { + // Namespace filtering is not needed: all incoming messages are forwarded via + // rawMessageReceived regardless of namespace. + Log.d(TAG, "registerNamespace: " + namespace); + } + + @Override + public void unregisterNamespace(String namespace) { + Log.d(TAG, "unregisterNamespace: " + namespace); + } + + // ---- ChromeCastConnectionEventListener ---- + + @Override + public void connectionEventReceived(ChromeCastConnectionEvent event) { + if (!event.isConnected()) { + onDisconnected(CommonStatusCodes.SUCCESS); } - metadata.senderAppIdentifier = this.context.getPackageName(); - return metadata; } + // ---- ChromeCastSpontaneousEventListener ---- + @Override public void spontaneousEventReceived(ChromeCastSpontaneousEvent event) { switch (event.getType()) { - case MEDIA_STATUS: - break; - case STATUS: - su.litvak.chromecast.api.v2.Status status = (su.litvak.chromecast.api.v2.Status)event.getData(); + case STATUS: { + su.litvak.chromecast.api.v2.Status status = + (su.litvak.chromecast.api.v2.Status) event.getData(); Application app = status.getRunningApp(); - ApplicationMetadata metadata = this.createMetadataFromApplication(app); + ApplicationMetadata metadata = createMetadataFromApplication(app); if (app != null) { - this.onApplicationStatusChanged(new ApplicationStatus(app.statusText)); + onApplicationStatusChanged(new ApplicationStatus(app.statusText)); } - int activeInputState = status.activeInput ? 1 : 0; - int standbyState = status.standBy ? 1 : 0; - this.onDeviceStatusChanged(new CastDeviceStatus(status.volume.level, status.volume.muted, activeInputState, metadata, standbyState)); - break; - case APPEVENT: + int activeInput = status.activeInput ? 1 : 0; + int standby = status.standBy ? 1 : 0; + onDeviceStatusChanged(new CastDeviceStatus( + status.volume.level, status.volume.muted, activeInput, metadata, standby)); break; + } case CLOSE: - this.onApplicationDisconnected(CommonStatusCodes.SUCCESS); + onApplicationDisconnected(CommonStatusCodes.SUCCESS); break; default: break; } } + // ---- ChromeCastRawMessageListener ---- + @Override public void rawMessageReceived(ChromeCastRawMessage message, Long requestId) { switch (message.getPayloadType()) { case STRING: - String response = message.getPayloadUtf8(); + String payload = message.getPayloadUtf8(); if (requestId == null) { - this.onTextMessageReceived(message.getNamespace(), response); + onTextMessageReceived(message.getNamespace(), payload); } else { - this.onSendMessageSuccess(response, requestId); - this.onTextMessageReceived(message.getNamespace(), response); + onSendMessageSuccess(payload, requestId); + onTextMessageReceived(message.getNamespace(), payload); } break; case BINARY: - byte[] payload = message.getPayloadBinary(); - this.onBinaryMessageReceived(message.getNamespace(), payload); + onBinaryMessageReceived(message.getNamespace(), message.getPayloadBinary()); break; } } - @Override - public void disconnect() { - try { - this.chromecast.disconnect(); - } catch (IOException e) { - Log.e(TAG, "Error disconnecting chromecast: " + e.getMessage()); - return; - } - } - - @Override - public void sendMessage(String namespace, String message, long requestId) { - try { - this.chromecast.sendRawRequest(namespace, message, requestId); - } catch (IOException e) { - Log.w(TAG, "Error sending cast message: " + e.getMessage()); - this.onSendMessageFailure("", requestId, CommonStatusCodes.NETWORK_ERROR); - return; - } - } + // ---- Helpers ---- - @Override - public void stopApplication(String sessionId) { - try { - this.chromecast.stopSession(sessionId); - } catch (IOException e) { - Log.w(TAG, "Error sending cast message: " + e.getMessage()); - return; + private ApplicationMetadata createMetadataFromApplication(Application app) { + if (app == null) return null; + ApplicationMetadata metadata = new ApplicationMetadata(); + metadata.applicationId = app.id; + metadata.name = app.name; + metadata.images = new ArrayList(); + metadata.namespaces = new ArrayList(); + for (Namespace ns : app.namespaces) { + metadata.namespaces.add(ns.name); } - this.sessionId = null; - } - - @Override - public void registerNamespace(String namespace) { - Log.d(TAG, "unimplemented Method: registerNamespace"); + metadata.senderAppIdentifier = context.getPackageName(); + return metadata; } - @Override - public void unregisterNamespace(String namespace) { - Log.d(TAG, "unimplemented Method: unregisterNamespace"); - } + // ---- Listener dispatch ---- - @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()); - this.onApplicationConnectionFailure(CommonStatusCodes.NETWORK_ERROR); - return; + public void onConnected(String sessionId) { + if (listener != null) { + try { + listener.onConnected(sessionId); + } catch (RemoteException ex) { + Log.e(TAG, "Error calling onConnected: " + ex.getMessage()); + } } - this.sessionId = app.sessionId; - - ApplicationMetadata metadata = this.createMetadataFromApplication(app); - this.onApplicationConnectionSuccess(metadata, app.statusText, app.sessionId, true); - } - - @Override - public void joinApplication(String applicationId, String sessionId, JoinOptions joinOptions) { - Log.d(TAG, "unimplemented Method: joinApplication"); - this.launchApplication(applicationId, new LaunchOptions()); } public void onDisconnected(int reason) { - if (this.listener != null) { + if (listener != null) { try { - this.listener.onDisconnected(reason); + listener.onDisconnected(reason); } catch (RemoteException ex) { Log.e(TAG, "Error calling onDisconnected: " + ex.getMessage()); } } } - public void onApplicationConnectionSuccess(ApplicationMetadata applicationMetadata, String applicationStatus, String sessionId, boolean wasLaunched) { - if (this.listener != null) { + public void onApplicationConnectionSuccess(ApplicationMetadata metadata, String appStatus, + String sessionId, boolean wasLaunched) { + if (listener != null) { try { - this.listener.onApplicationConnectionSuccess(applicationMetadata, applicationStatus, sessionId, wasLaunched); + listener.onApplicationConnectionSuccess(metadata, appStatus, sessionId, wasLaunched); } catch (RemoteException ex) { Log.e(TAG, "Error calling onApplicationConnectionSuccess: " + ex.getMessage()); } @@ -246,80 +338,79 @@ public void onApplicationConnectionSuccess(ApplicationMetadata applicationMetada } public void onApplicationConnectionFailure(int statusCode) { - if (this.listener != null) { + if (listener != null) { try { - this.listener.onApplicationConnectionFailure(statusCode); + listener.onApplicationConnectionFailure(statusCode); } catch (RemoteException ex) { Log.e(TAG, "Error calling onApplicationConnectionFailure: " + ex.getMessage()); } } } - public void onTextMessageReceived(String namespace, String message) { - if (this.listener != null) { + public void onApplicationDisconnected(int code) { + if (listener != null) { try { - this.listener.onTextMessageReceived(namespace, message); + listener.onApplicationDisconnected(code); } catch (RemoteException ex) { - Log.e(TAG, "Error calling onTextMessageReceived: " + ex.getMessage()); + Log.e(TAG, "Error calling onApplicationDisconnected: " + ex.getMessage()); } } } - public void onBinaryMessageReceived(String namespace, byte[] data) { - if (this.listener != null) { + public void onTextMessageReceived(String namespace, String message) { + if (listener != null) { try { - this.listener.onBinaryMessageReceived(namespace, data); + listener.onTextMessageReceived(namespace, message); } catch (RemoteException ex) { - Log.e(TAG, "Error calling onBinaryMessageReceived: " + ex.getMessage()); + Log.e(TAG, "Error calling onTextMessageReceived: " + ex.getMessage()); } } } - public void onApplicationDisconnected(int paramInt) { - Log.d(TAG, "unimplemented Method: onApplicationDisconnected"); - if (this.listener != null) { + public void onBinaryMessageReceived(String namespace, byte[] data) { + if (listener != null) { try { - this.listener.onApplicationDisconnected(paramInt); + listener.onBinaryMessageReceived(namespace, data); } catch (RemoteException ex) { - Log.e(TAG, "Error calling onApplicationDisconnected: " + ex.getMessage()); + Log.e(TAG, "Error calling onBinaryMessageReceived: " + ex.getMessage()); } } } - public void onSendMessageFailure(String response, long requestId, int statusCode) { - if (this.listener != null) { + public void onSendMessageSuccess(String response, long requestId) { + if (listener != null) { try { - this.listener.onSendMessageFailure(response, requestId, statusCode); + listener.onSendMessageSuccess(response, requestId); } catch (RemoteException ex) { - Log.e(TAG, "Error calling onSendMessageFailure: " + ex.getMessage()); + Log.e(TAG, "Error calling onSendMessageSuccess: " + ex.getMessage()); } } } - public void onSendMessageSuccess(String response, long requestId) { - if (this.listener != null) { + public void onSendMessageFailure(String response, long requestId, int statusCode) { + if (listener != null) { try { - this.listener.onSendMessageSuccess(response, requestId); + listener.onSendMessageFailure(response, requestId, statusCode); } catch (RemoteException ex) { - Log.e(TAG, "Error calling onSendMessageSuccess: " + ex.getMessage()); + Log.e(TAG, "Error calling onSendMessageFailure: " + ex.getMessage()); } } } - public void onApplicationStatusChanged(ApplicationStatus applicationStatus) { - if (this.listener != null) { + public void onApplicationStatusChanged(ApplicationStatus status) { + if (listener != null) { try { - this.listener.onApplicationStatusChanged(applicationStatus); + listener.onApplicationStatusChanged(status); } catch (RemoteException ex) { Log.e(TAG, "Error calling onApplicationStatusChanged: " + ex.getMessage()); } } } - public void onDeviceStatusChanged(CastDeviceStatus deviceStatus) { - if (this.listener != null) { + public void onDeviceStatusChanged(CastDeviceStatus status) { + if (listener != null) { try { - this.listener.onDeviceStatusChanged(deviceStatus); + listener.onDeviceStatusChanged(status); } catch (RemoteException ex) { Log.e(TAG, "Error calling onDeviceStatusChanged: " + ex.getMessage()); } diff --git a/play-services-cast/core/src/main/java/org/microg/gms/cast/CastDeviceControllerService.java b/play-services-cast/core/src/main/java/org/microg/gms/cast/CastDeviceControllerService.java index d494a012af..21a7073e65 100644 --- a/play-services-cast/core/src/main/java/org/microg/gms/cast/CastDeviceControllerService.java +++ b/play-services-cast/core/src/main/java/org/microg/gms/cast/CastDeviceControllerService.java @@ -16,35 +16,41 @@ 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(); + /** + * Feature flag required by the Cast SDK client for establishing a connection. + * Without this, the client rejects the service binder before connect() is called, + * causing a silent failure with no Cast button shown. + */ + private static final String FEATURE_CXLESS_CLIENT_MINIMAL = "cxless_client_minimal"; + 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); + public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, + GmsService service) throws RemoteException { + // Advertise required feature flags so the Cast SDK does not abort the connection. + if (request.extras != null + && !request.extras.containsKey(FEATURE_CXLESS_CLIENT_MINIMAL)) { + request.extras.putBoolean(FEATURE_CXLESS_CLIENT_MINIMAL, true); + } + + CastDeviceControllerImpl controller = + new CastDeviceControllerImpl(this, request.packageName, request.extras); + + callback.onPostInitComplete(0, controller, null); } } diff --git a/play-services-cast/core/src/main/java/org/microg/gms/cast/CastMediaRouteController.java b/play-services-cast/core/src/main/java/org/microg/gms/cast/CastMediaRouteController.java index f8ca7a1a59..92668296f9 100644 --- a/play-services-cast/core/src/main/java/org/microg/gms/cast/CastMediaRouteController.java +++ b/play-services-cast/core/src/main/java/org/microg/gms/cast/CastMediaRouteController.java @@ -16,78 +16,131 @@ 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.util.Log; import androidx.mediarouter.media.MediaRouteProvider; import androidx.mediarouter.media.MediaRouter; -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; + // MediaRouter volumes are integers in [0, VOLUME_MAX]; ChromeCast uses floats in [0.0, 1.0]. + private static final int VOLUME_MAX = 20; - public CastMediaRouteController(CastMediaRouteProvider provider, String routeId, String address) { - super(); + private final CastMediaRouteProvider provider; + private final String routeId; + private final ChromeCast chromecast; + public CastMediaRouteController(CastMediaRouteProvider provider, + String routeId, String address) { this.provider = provider; this.routeId = routeId; this.chromecast = new ChromeCast(address); } - public boolean onControlRequest(Intent intent, MediaRouter.ControlRequestCallback callback) { - Log.d(TAG, "unimplemented Method: onControlRequest: " + this.routeId); - return false; + /** + * Called when the user selects this route. Pre-connect so that the subsequent + * launchApplication or joinApplication call completes faster. + */ + @Override + public void onSelect() { + Log.d(TAG, "onSelect: " + routeId); + new Thread(() -> { + try { + if (!chromecast.isConnected()) { + chromecast.connect(); + } + } catch (IOException e) { + Log.w(TAG, "Pre-connect on select failed: " + e.getMessage()); + } + }, "CastRouteSelect-" + routeId).start(); } - public void onRelease() { - Log.d(TAG, "unimplemented Method: onRelease: " + this.routeId); + @Override + public void onUnselect() { + onUnselect(MediaRouter.UNSELECT_REASON_UNKNOWN); } - public void onSelect() { - Log.d(TAG, "unimplemented Method: onSelect: " + this.routeId); + /** + * Called when the route is deselected. Disconnects the underlying transport cleanly. + */ + @Override + public void onUnselect(int reason) { + Log.d(TAG, "onUnselect reason=" + reason + " route=" + routeId); + disconnectAsync(); } + /** + * Called when this RouteController is permanently released. Disconnect if still connected. + */ + @Override + public void onRelease() { + Log.d(TAG, "onRelease: " + routeId); + disconnectAsync(); + } + + /** + * Sets the absolute volume level (0 – VOLUME_MAX). + */ + @Override public void onSetVolume(int volume) { - Log.d(TAG, "unimplemented Method: onSetVolume: " + this.routeId); + float normalized = Math.max(0f, Math.min(1f, (float) volume / VOLUME_MAX)); + new Thread(() -> { + try { + if (chromecast.isConnected()) { + chromecast.setVolume(normalized); + } + } catch (IOException e) { + Log.w(TAG, "Error setting volume: " + e.getMessage()); + } + }, "CastSetVolume-" + routeId).start(); } - public void onUnselect() { - Log.d(TAG, "unimplemented Method: onUnselect: " + this.routeId); + /** + * Adjusts the volume by a relative delta (positive = louder, negative = quieter). + */ + @Override + public void onUpdateVolume(int delta) { + new Thread(() -> { + try { + if (!chromecast.isConnected()) return; + Status status = chromecast.getStatus(); + if (status != null && status.volume != null) { + float current = (float) status.volume.level; + float step = (float) delta / VOLUME_MAX; + float next = Math.max(0f, Math.min(1f, current + step)); + chromecast.setVolume(next); + } + } catch (IOException e) { + Log.w(TAG, "Error updating volume: " + e.getMessage()); + } + }, "CastUpdateVolume-" + routeId).start(); } - public void onUnselect(int reason) { - Log.d(TAG, "unimplemented Method: onUnselect: " + this.routeId); + /** + * Media control requests (play/pause/seek/etc.) are handled by the Cast SDK layer via + * CastDeviceControllerImpl, not directly here. Return false so MediaRouter passes them up. + */ + @Override + public boolean onControlRequest(Intent intent, MediaRouter.ControlRequestCallback callback) { + return false; } - public void onUpdateVolume(int delta) { - Log.d(TAG, "unimplemented Method: onUpdateVolume: " + this.routeId); + private void disconnectAsync() { + new Thread(() -> { + try { + if (chromecast.isConnected()) { + chromecast.disconnect(); + } + } catch (IOException e) { + Log.w(TAG, "Error disconnecting on unselect/release: " + e.getMessage()); + } + }, "CastDisconnect-" + routeId).start(); } } diff --git a/play-services-cast/core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java b/play-services-cast/core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java index 89efb07929..05372f5929 100644 --- a/play-services-cast/core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java +++ b/play-services-cast/core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java @@ -19,11 +19,9 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.IntentFilter; -import android.net.Uri; import android.net.nsd.NsdManager; import android.net.nsd.NsdServiceInfo; import android.os.Bundle; -import android.os.AsyncTask; import android.os.Handler; import android.util.Log; @@ -34,33 +32,29 @@ import androidx.mediarouter.media.MediaRouteProviderDescriptor; import androidx.mediarouter.media.MediaRouter; -import com.google.android.gms.common.images.WebImage; import com.google.android.gms.cast.CastDevice; import com.google.android.gms.cast.CastMediaControlIntent; +import com.google.android.gms.common.images.WebImage; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Inet4Address; -import java.net.UnknownHostException; -import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.lang.Thread; -import java.lang.Runnable; -import java.util.List; +import java.net.InetAddress; import java.util.ArrayList; +import java.util.List; import java.util.Map; -import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; public class CastMediaRouteProvider extends MediaRouteProvider { private static final String TAG = CastMediaRouteProvider.class.getSimpleName(); - private Map castDevices = new HashMap(); - private Map serviceCastIds = new HashMap(); + // ConcurrentHashMap prevents ConcurrentModificationException when NSD resolution + // callbacks add/remove devices while the main thread iterates in publishRoutes(). + private final Map castDevices = new ConcurrentHashMap<>(); + private final Map serviceCastIds = new ConcurrentHashMap<>(); private NsdManager mNsdManager; private NsdManager.DiscoveryListener mDiscoveryListener; - private List customCategories = new ArrayList(); + private final List customCategories = new ArrayList<>(); private enum State { NOT_DISCOVERING, @@ -68,9 +62,11 @@ private enum State { DISCOVERING, DISCOVERY_STOP_REQUESTED, } + private State state = State.NOT_DISCOVERING; - private static final ArrayList BASE_CONTROL_FILTERS = new ArrayList(); + private static final ArrayList BASE_CONTROL_FILTERS = new ArrayList<>(); + static { IntentFilter filter; @@ -84,35 +80,15 @@ private enum State { filter.addDataScheme("http"); filter.addDataScheme("https"); String[] types = { - "image/jpeg", - "image/pjpeg", - "image/jpg", - "image/webp", - "image/png", - "image/gif", - "image/bmp", - "image/vnd.microsoft.icon", - "image/x-icon", - "image/x-xbitmap", - "audio/wav", - "audio/x-wav", - "audio/mp3", - "audio/x-mp3", - "audio/x-m4a", - "audio/mpeg", - "audio/webm", - "audio/ogg", - "audio/x-matroska", - "video/mp4", - "video/x-m4v", - "video/mp2t", - "video/webm", - "video/ogg", - "video/x-matroska", - "application/x-mpegurl", - "application/vnd.apple.mpegurl", - "application/dash+xml", - "application/vnd.ms-sstr+xml", + "image/jpeg", "image/pjpeg", "image/jpg", "image/webp", "image/png", + "image/gif", "image/bmp", "image/vnd.microsoft.icon", + "image/x-icon", "image/x-xbitmap", + "audio/wav", "audio/x-wav", "audio/mp3", "audio/x-mp3", "audio/x-m4a", + "audio/mpeg", "audio/webm", "audio/ogg", "audio/x-matroska", + "video/mp4", "video/x-m4v", "video/mp2t", "video/webm", + "video/ogg", "video/x-matroska", + "application/x-mpegurl", "application/vnd.apple.mpegurl", + "application/dash+xml", "application/vnd.ms-sstr+xml", }; for (String type : types) { try { @@ -167,11 +143,6 @@ private enum State { filter.addCategory(CastMediaControlIntent.CATEGORY_CAST_REMOTE_PLAYBACK); filter.addAction(CastMediaControlIntent.ACTION_SYNC_STATUS); BASE_CONTROL_FILTERS.add(filter); - - filter = new IntentFilter(); - filter.addCategory(CastMediaControlIntent.CATEGORY_CAST_REMOTE_PLAYBACK); - filter.addAction(CastMediaControlIntent.ACTION_SYNC_STATUS); - BASE_CONTROL_FILTERS.add(filter); } @SuppressLint("NewApi") @@ -183,7 +154,8 @@ public CastMediaRouteProvider(Context context) { return; } - mNsdManager = (NsdManager)context.getApplicationContext().getSystemService(Context.NSD_SERVICE); + mNsdManager = (NsdManager) context.getApplicationContext() + .getSystemService(Context.NSD_SERVICE); mDiscoveryListener = new NsdManager.DiscoveryListener() { @@ -198,9 +170,10 @@ public void onServiceFound(NsdServiceInfo service) { @Override public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { if (errorCode == NsdManager.FAILURE_ALREADY_ACTIVE) { + // Resolution already in progress for this service; ignore. return; } - Log.e(TAG, "DiscoveryListener Resolve failed. Error code " + errorCode); + Log.e(TAG, "Resolve failed. Error code: " + errorCode); } @Override @@ -210,7 +183,7 @@ public void onServiceResolved(NsdServiceInfo serviceInfo) { int port = serviceInfo.getPort(); Map attributes = serviceInfo.getAttributes(); if (attributes == null) { - Log.e(TAG, "Error getting service attributes from DNS-SD response"); + Log.e(TAG, "Missing DNS-SD attributes"); return; } try { @@ -219,12 +192,12 @@ public void onServiceResolved(NsdServiceInfo serviceInfo) { String friendlyName = new String(attributes.get("fn"), "UTF-8"); String modelName = new String(attributes.get("md"), "UTF-8"); String iconPath = new String(attributes.get("ic"), "UTF-8"); - int status = Integer.parseInt(new String(attributes.get("st"), "UTF-8")); - - onChromeCastDiscovered(id, name, host, port, deviceVersion, friendlyName, modelName, iconPath, status); + int status = Integer.parseInt( + new String(attributes.get("st"), "UTF-8")); + onChromeCastDiscovered(id, name, host, port, deviceVersion, + friendlyName, modelName, iconPath, status); } catch (UnsupportedEncodingException | NullPointerException ex) { - Log.e(TAG, "Error getting cast details from DNS-SD response", ex); - return; + Log.e(TAG, "Error parsing DNS-SD response", ex); } } }); @@ -232,8 +205,7 @@ public void onServiceResolved(NsdServiceInfo serviceInfo) { @Override public void onServiceLost(NsdServiceInfo serviceInfo) { - String name = serviceInfo.getServiceName(); - onChromeCastLost(name); + onChromeCastLost(serviceInfo.getServiceName()); } @Override @@ -243,94 +215,87 @@ public void onDiscoveryStopped(String serviceType) { @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { + Log.e(TAG, "Start discovery failed: " + errorCode); CastMediaRouteProvider.this.state = State.NOT_DISCOVERING; } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { + Log.e(TAG, "Stop discovery failed: " + errorCode); CastMediaRouteProvider.this.state = State.DISCOVERING; } }; } - private void onChromeCastDiscovered( - String id, String name, InetAddress host, int port, String - deviceVersion, String friendlyName, String modelName, String - iconPath, int status) { - if (!this.castDevices.containsKey(id)) { - // TODO: Capabilities + private void onChromeCastDiscovered(String id, String name, InetAddress host, int port, + String deviceVersion, String friendlyName, String modelName, + String iconPath, int status) { + if (!castDevices.containsKey(id)) { int capabilities = CastDevice.CAPABILITY_VIDEO_OUT | CastDevice.CAPABILITY_AUDIO_OUT; - - CastDevice castDevice = new CastDevice(id, name, host, port, deviceVersion, friendlyName, modelName, iconPath, status, capabilities); - this.castDevices.put(id, castDevice); - this.serviceCastIds.put(name, id); + CastDevice castDevice = new CastDevice(id, name, host, port, deviceVersion, + friendlyName, modelName, iconPath, status, capabilities); + castDevices.put(id, castDevice); + serviceCastIds.put(name, id); } - publishRoutesInMainThread(); } private void onChromeCastLost(String name) { - String id = this.serviceCastIds.remove(name); + String id = serviceCastIds.remove(name); if (id != null) { - this.castDevices.remove(id); + castDevices.remove(id); } - publishRoutesInMainThread(); } @SuppressLint("NewApi") @Override public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) { - if (android.os.Build.VERSION.SDK_INT < 16) { - return; - } + if (android.os.Build.VERSION.SDK_INT < 16) return; if (request != null && request.isValid() && request.isActiveScan()) { if (request.getSelector() != null) { for (String category : request.getSelector().getControlCategories()) { - if (CastMediaControlIntent.isCategoryForCast(category)) { - this.customCategories.add(category); + if (CastMediaControlIntent.isCategoryForCast(category) + && !customCategories.contains(category)) { + customCategories.add(category); } } } - if (this.state == State.NOT_DISCOVERING) { - mNsdManager.discoverServices("_googlecast._tcp.", NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener); - this.state = State.DISCOVERY_REQUESTED; + if (state == State.NOT_DISCOVERING) { + mNsdManager.discoverServices( + "_googlecast._tcp.", NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener); + state = State.DISCOVERY_REQUESTED; } } else { - if (this.state == State.DISCOVERING) { + if (state == State.DISCOVERING) { mNsdManager.stopServiceDiscovery(mDiscoveryListener); - this.state = State.DISCOVERY_STOP_REQUESTED; + state = State.DISCOVERY_STOP_REQUESTED; } } } @Override public RouteController onCreateRouteController(String routeId) { - CastDevice castDevice = this.castDevices.get(routeId); - if (castDevice == null) { - return null; - } + CastDevice castDevice = castDevices.get(routeId); + if (castDevice == null) return null; return new CastMediaRouteController(this, routeId, castDevice.getAddress()); } private void publishRoutesInMainThread() { - Handler mainHandler = new Handler(this.getContext().getMainLooper()); - mainHandler.post(new Runnable() { - @Override - public void run() { - publishRoutes(); - } - }); + new Handler(getContext().getMainLooper()).post(this::publishRoutes); } private void publishRoutes() { MediaRouteProviderDescriptor.Builder builder = new MediaRouteProviderDescriptor.Builder(); - for (CastDevice castDevice : this.castDevices.values()) { - ArrayList controlFilters = new ArrayList(BASE_CONTROL_FILTERS); - // Include any app-specific control filters that have been requested. - // TODO: Do we need to check with the device? - for (String category : this.customCategories) { + + // Snapshot the values to avoid ConcurrentModificationException if the map is + // updated by an NSD callback while we iterate. + List snapshot = new ArrayList<>(castDevices.values()); + + for (CastDevice castDevice : snapshot) { + ArrayList controlFilters = new ArrayList<>(BASE_CONTROL_FILTERS); + for (String category : customCategories) { IntentFilter filter = new IntentFilter(); filter.addCategory(category); controlFilters.add(filter); @@ -338,22 +303,23 @@ private void publishRoutes() { Bundle extras = new Bundle(); castDevice.putInBundle(extras); + MediaRouteDescriptor route = new MediaRouteDescriptor.Builder( - castDevice.getDeviceId(), - castDevice.getFriendlyName()) - .setDescription(castDevice.getModelName()) - .addControlFilters(controlFilters) - .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(); + castDevice.getDeviceId(), + castDevice.getFriendlyName()) + .setDescription(castDevice.getModelName()) + .addControlFilters(controlFilters) + .setDeviceType(MediaRouter.RouteInfo.DEVICE_TYPE_TV) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) + .setVolumeMax(20) + .setVolume(0) + .setEnabled(true) + .setExtras(extras) + .setConnectionState(MediaRouter.RouteInfo.CONNECTION_STATE_DISCONNECTED) + .build(); builder.addRoute(route); } - this.setDescriptor(builder.build()); + setDescriptor(builder.build()); } } diff --git a/play-services-cast/src/main/aidl/com/google/android/gms/cast/internal/ICastDeviceController.aidl b/play-services-cast/src/main/aidl/com/google/android/gms/cast/internal/ICastDeviceController.aidl index 4f91cdda20..12c72204ee 100644 --- a/play-services-cast/src/main/aidl/com/google/android/gms/cast/internal/ICastDeviceController.aidl +++ b/play-services-cast/src/main/aidl/com/google/android/gms/cast/internal/ICastDeviceController.aidl @@ -11,4 +11,6 @@ interface ICastDeviceController { oneway void unregisterNamespace(String namespace) = 11; oneway void launchApplication(String applicationId, in LaunchOptions launchOptions) = 12; oneway void joinApplication(String applicationId, String sessionId, in JoinOptions joinOptions) = 13; + oneway void connect() = 16; + oneway void addListener(IBinder listener) = 17; } diff --git a/play-services-cast/src/main/aidl/com/google/android/gms/cast/internal/ICastDeviceControllerListener.aidl b/play-services-cast/src/main/aidl/com/google/android/gms/cast/internal/ICastDeviceControllerListener.aidl index 1d26c14b03..a18ec5a201 100644 --- a/play-services-cast/src/main/aidl/com/google/android/gms/cast/internal/ICastDeviceControllerListener.aidl +++ b/play-services-cast/src/main/aidl/com/google/android/gms/cast/internal/ICastDeviceControllerListener.aidl @@ -8,14 +8,12 @@ interface ICastDeviceControllerListener { void onDisconnected(int reason) = 0; void onApplicationConnectionSuccess(in ApplicationMetadata applicationMetadata, String applicationStatus, String sessionId, boolean wasLaunched) = 1; void onApplicationConnectionFailure(int statusCode) = 2; - // Deprecated: void onStatusReceived(String string1, double double1, boolean boolean1) = 3; void onTextMessageReceived(String namespace, String message) = 4; void onBinaryMessageReceived(String namespace, in byte[] data) = 5; - // void onStatusChanged(int status) = 6; // TODO - // void onStatusChanged2(int status) = 7; // TODO void onApplicationDisconnected(int paramInt) = 8; void onSendMessageFailure(String response, long requestId, int statusCode) = 9; void onSendMessageSuccess(String response, long requestId) = 10; void onApplicationStatusChanged(in ApplicationStatus applicationStatus) = 11; void onDeviceStatusChanged(in CastDeviceStatus deviceStatus) = 12; + void onConnected(String sessionId) = 13; }