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..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,15 +17,12 @@ package org.microg.gms.cast; import java.io.IOException; +import java.security.GeneralSecurityException; 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 +34,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 +46,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 +85,22 @@ 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()) { + try { + this.chromecast.connect(); + } catch (GeneralSecurityException e) { + throw new IOException("SSL error connecting to cast device", e); + } + } + } + @Override public void connectionEventReceived(ChromeCastConnectionEvent event) { if (!event.isConnected()) { @@ -109,7 +119,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 +132,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 +177,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 +215,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 +231,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 +317,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); 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..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 @@ -16,36 +16,16 @@ 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 java.security.GeneralSecurityException; 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 +36,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 | GeneralSecurityException 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); }