From 729ebdcbcbacf9ce71d97d62cc5e2328ed3228e2 Mon Sep 17 00:00:00 2001 From: FloodExLLC Date: Sat, 21 Mar 2026 14:10:29 -0400 Subject: [PATCH 1/4] Cast: establish TCP connection before device operations The ChromeCast object was created but connect() was never called before launchApp(), sendRawRequest() etc., causing all outgoing operations to fail with IOException because no socket existed. - Add ensureConnected() helper; call it at the top of every outgoing operation (launchApplication, joinApplication, sendMessage, stopApplication) - Fix launchApplication() to guard against a null Application response instead of NPE-ing on app.sessionId - Implement joinApplication() properly: query device status, join an already-running matching session (wasLaunched=false), and only fall back to launching when the app is absent --- .../gms/cast/CastDeviceControllerImpl.java | 67 ++++++++++++++----- 1 file changed, 51 insertions(+), 16 deletions(-) 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..d43ab31e95 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,10 @@ 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; @@ -37,10 +33,8 @@ 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; @@ -51,7 +45,6 @@ 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; public class CastDeviceControllerImpl extends ICastDeviceController.Stub implements ChromeCastConnectionEventListener, @@ -91,6 +84,18 @@ public CastDeviceControllerImpl(Context context, String packageName, Bundle extr this.chromecast.registerConnectionListener(this); } + /** + * Ensures a TCP/TLS connection to the Cast device is established. + * Must be called before any operation that communicates with the device. + * + * @throws IOException if the connection cannot be established + */ + private void ensureConnected() throws IOException { + if (!this.chromecast.isConnected()) { + this.chromecast.connect(); + } + } + @Override public void connectionEventReceived(ChromeCastConnectionEvent event) { if (!event.isConnected()) { @@ -109,7 +114,7 @@ protected ApplicationMetadata createMetadataFromApplication(Application app) { Log.d(TAG, "unimplemented: ApplicationMetadata.senderAppLaunchUri"); metadata.images = new ArrayList(); metadata.namespaces = new ArrayList(); - for(Namespace namespace : app.namespaces) { + for (Namespace namespace : app.namespaces) { metadata.namespaces.add(namespace.name); } metadata.senderAppIdentifier = this.context.getPackageName(); @@ -122,7 +127,7 @@ public void spontaneousEventReceived(ChromeCastSpontaneousEvent event) { case MEDIA_STATUS: break; case STATUS: - su.litvak.chromecast.api.v2.Status status = (su.litvak.chromecast.api.v2.Status)event.getData(); + su.litvak.chromecast.api.v2.Status status = (su.litvak.chromecast.api.v2.Status) event.getData(); Application app = status.getRunningApp(); ApplicationMetadata metadata = this.createMetadataFromApplication(app); if (app != null) { @@ -167,27 +172,27 @@ public void disconnect() { 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 { + ensureConnected(); 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; } } @Override public void stopApplication(String sessionId) { try { + ensureConnected(); this.chromecast.stopSession(sessionId); } catch (IOException e) { - Log.w(TAG, "Error sending cast message: " + e.getMessage()); + Log.w(TAG, "Error stopping cast session: " + e.getMessage()); return; } this.sessionId = null; @@ -205,6 +210,14 @@ public void unregisterNamespace(String namespace) { @Override public void launchApplication(String applicationId, LaunchOptions launchOptions) { + try { + ensureConnected(); + } catch (IOException e) { + Log.w(TAG, "Error connecting to cast device: " + e.getMessage()); + this.onApplicationConnectionFailure(CommonStatusCodes.NETWORK_ERROR); + return; + } + Application app = null; try { app = this.chromecast.launchApp(applicationId); @@ -213,16 +226,39 @@ public void launchApplication(String applicationId, LaunchOptions launchOptions) this.onApplicationConnectionFailure(CommonStatusCodes.NETWORK_ERROR); return; } - this.sessionId = app.sessionId; + if (app == null) { + Log.w(TAG, "launchApplication returned null for id: " + applicationId); + this.onApplicationConnectionFailure(CommonStatusCodes.ERROR); + return; + } + + 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()); + try { + ensureConnected(); + su.litvak.chromecast.api.v2.Status status = this.chromecast.getStatus(); + Application runningApp = (status != null) ? status.getRunningApp() : null; + + if (runningApp != null && runningApp.id.equals(applicationId) + && (sessionId == null || runningApp.sessionId.equals(sessionId))) { + // The requested app is already running — join it without relaunching. + this.sessionId = runningApp.sessionId; + ApplicationMetadata metadata = this.createMetadataFromApplication(runningApp); + this.onApplicationConnectionSuccess(metadata, runningApp.statusText, runningApp.sessionId, false); + } else { + // App not running or session mismatch — fall back to launching. + this.launchApplication(applicationId, new LaunchOptions()); + } + } catch (IOException e) { + Log.w(TAG, "Error joining cast application: " + e.getMessage()); + this.onApplicationConnectionFailure(CommonStatusCodes.NETWORK_ERROR); + } } public void onDisconnected(int reason) { @@ -276,7 +312,6 @@ public void onBinaryMessageReceived(String namespace, byte[] data) { } public void onApplicationDisconnected(int paramInt) { - Log.d(TAG, "unimplemented Method: onApplicationDisconnected"); if (this.listener != null) { try { this.listener.onApplicationDisconnected(paramInt); From fc868410ab329a1bbe6f88fe64655c33c83d030e Mon Sep 17 00:00:00 2001 From: FloodExLLC Date: Sat, 21 Mar 2026 14:10:29 -0400 Subject: [PATCH 2/4] Cast: implement route controller connect/disconnect lifecycle onSelect() and onUnselect() were stubs. The Cast device connection lifecycle must mirror the route selection lifecycle so the socket is open before the Cast session begins and closed when the user switches away. - onSelect(): open TCP/TLS connection to the Cast device - onUnselect() / onUnselect(int): close the connection - onRelease(): close the connection when the controller is destroyed --- .../gms/cast/CastMediaRouteController.java | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) 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..33afd592c7 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,36 +16,15 @@ 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(); @@ -56,37 +35,69 @@ public class CastMediaRouteController extends MediaRouteProvider.RouteController public CastMediaRouteController(CastMediaRouteProvider provider, String routeId, String address) { super(); - this.provider = provider; this.routeId = routeId; this.chromecast = new ChromeCast(address); } + @Override public boolean onControlRequest(Intent intent, MediaRouter.ControlRequestCallback callback) { Log.d(TAG, "unimplemented Method: onControlRequest: " + this.routeId); return false; } + @Override public void onRelease() { - Log.d(TAG, "unimplemented Method: onRelease: " + this.routeId); + try { + if (this.chromecast.isConnected()) { + this.chromecast.disconnect(); + } + } catch (IOException e) { + Log.e(TAG, "Error releasing cast route controller: " + e.getMessage()); + } } + /** + * Called when the user selects this route. Opens the TCP/TLS connection + * to the Cast device so subsequent operations succeed immediately. + */ + @Override public void onSelect() { - Log.d(TAG, "unimplemented Method: onSelect: " + this.routeId); + try { + if (!this.chromecast.isConnected()) { + this.chromecast.connect(); + } + } catch (IOException e) { + Log.e(TAG, "Error connecting to cast device on route select: " + e.getMessage()); + } } + @Override public void onSetVolume(int volume) { Log.d(TAG, "unimplemented Method: onSetVolume: " + this.routeId); } + /** + * Called when the user deselects or disconnects from this route. + * Closes the TCP/TLS connection to the Cast device. + */ + @Override public void onUnselect() { - Log.d(TAG, "unimplemented Method: onUnselect: " + this.routeId); + try { + if (this.chromecast.isConnected()) { + this.chromecast.disconnect(); + } + } catch (IOException e) { + Log.e(TAG, "Error disconnecting from cast device on route unselect: " + e.getMessage()); + } } + @Override public void onUnselect(int reason) { - Log.d(TAG, "unimplemented Method: onUnselect: " + this.routeId); + onUnselect(); } + @Override public void onUpdateVolume(int delta) { Log.d(TAG, "unimplemented Method: onUpdateVolume: " + this.routeId); } From 36192139d61269e50e9f0fcc39851e00a731d873 Mon Sep 17 00:00:00 2001 From: FloodExLLC Date: Sun, 22 Mar 2026 09:04:05 -0400 Subject: [PATCH 3/4] fix(cast): catch GeneralSecurityException in ensureConnected() chromecast.connect() throws both IOException and GeneralSecurityException (a checked exception). The ensureConnected() helper only declared throws IOException, causing a compile error. Wrap the connect() call to catch GeneralSecurityException and rethrow as IOException so all callers remain unchanged. Fixes: https://github.com/microg/GmsCore/pull/3351 --- .../java/org/microg/gms/cast/CastDeviceControllerImpl.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 d43ab31e95..8654c07477 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 @@ -17,6 +17,7 @@ package org.microg.gms.cast; import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.ArrayList; import android.content.Context; @@ -92,7 +93,11 @@ public CastDeviceControllerImpl(Context context, String packageName, Bundle extr */ private void ensureConnected() throws IOException { if (!this.chromecast.isConnected()) { - this.chromecast.connect(); + try { + this.chromecast.connect(); + } catch (GeneralSecurityException e) { + throw new IOException("SSL error connecting to cast device", e); + } } } From 3837124c1cfc9837d485bc286de60f323ec43628 Mon Sep 17 00:00:00 2001 From: FloodExLLC Date: Sun, 22 Mar 2026 09:45:30 -0400 Subject: [PATCH 4/4] fix(cast): catch GeneralSecurityException in CastMediaRouteController.onSelect() Same fix as CastDeviceControllerImpl: chromecast.connect() throws both IOException and GeneralSecurityException. Update the catch clause to handle both using a multi-catch. --- .../java/org/microg/gms/cast/CastMediaRouteController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 33afd592c7..e90d7d2863 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 @@ -23,6 +23,7 @@ import androidx.mediarouter.media.MediaRouter; import java.io.IOException; +import java.security.GeneralSecurityException; import su.litvak.chromecast.api.v2.ChromeCast; @@ -67,7 +68,7 @@ public void onSelect() { if (!this.chromecast.isConnected()) { this.chromecast.connect(); } - } catch (IOException e) { + } catch (IOException | GeneralSecurityException e) { Log.e(TAG, "Error connecting to cast device on route select: " + e.getMessage()); } }