From 6a0ac7b8c341f21685748c112d44d686e58bab95 Mon Sep 17 00:00:00 2001 From: Naorem Nganthoiba Singh Date: Fri, 4 Sep 2026 23:46:46 +0530 Subject: [PATCH] Add connectionless Cast API support (cxless_connect, cxless_set_listener) --- gradle.properties | 1 + .../framework/internal/CastContextImpl.java | 46 ++++++-- .../src/main/AndroidManifest.xml | 21 ++-- .../gms/cast/CastDeviceControllerImpl.java | 104 ++++++++++++++++-- .../gms/cast/CastDeviceControllerService.java | 31 +++--- .../cast/internal/ICastDeviceController.aidl | 6 +- .../ICastDeviceControllerListener.aidl | 1 + .../google/android/gms/cast/CastDevice.java | 4 +- 8 files changed, 170 insertions(+), 44 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1c50760c87..456cd7cd80 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,3 +2,4 @@ android.useAndroidX=true org.gradle.configuration-cache=true org.gradle.caching=true org.gradle.jvmargs=-Xmx4096m -XX:+UseParallelGC --add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +kotlin.daemon.jvmargs=-Xmx4g \ No newline at end of file diff --git a/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java b/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java index be1c5935c0..84681a7a16 100644 --- a/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java +++ b/play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java @@ -24,6 +24,7 @@ import androidx.mediarouter.media.MediaControlIntent; import androidx.mediarouter.media.MediaRouteSelector; +import androidx.mediarouter.media.MediaRouter; import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.framework.CastOptions; @@ -35,8 +36,8 @@ import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.dynamic.ObjectWrapper; -import java.util.Map; import java.util.HashMap; +import java.util.Map; public class CastContextImpl extends ICastContext.Stub { private static final String TAG = CastContextImpl.class.getSimpleName(); @@ -56,21 +57,51 @@ public CastContextImpl(IObjectWrapper context, CastOptions options, IMediaRouter this.context = (Context) ObjectWrapper.unwrap(context); this.options = options; this.router = router; + for (Map.Entry entry : sessionProviders.entrySet()) { - this.sessionProviders.put(entry.getKey(), ISessionProvider.Stub.asInterface(entry.getValue())); + this.sessionProviders.put( + entry.getKey(), + ISessionProvider.Stub.asInterface(entry.getValue()) + ); } String receiverApplicationId = options.getReceiverApplicationId(); String defaultCategory = CastMediaControlIntent.categoryForCast(receiverApplicationId); + // Direct lookup this.defaultSessionProvider = this.sessionProviders.get(defaultCategory); + // Prefix fallback + if (this.defaultSessionProvider == null) { + for (Map.Entry entry : this.sessionProviders.entrySet()) { + if (entry.getKey() != null && entry.getKey().startsWith(defaultCategory)) { + this.defaultSessionProvider = entry.getValue(); + break; + } + } + } + // TODO: This should incorporate passed options this.mergedSelector = new MediaRouteSelector.Builder() - .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) - .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) - .addControlCategory(defaultCategory) - .build(); + .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) + .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) + .addControlCategory(defaultCategory) + .build(); + + // Always register the media router callback + try { + this.router.registerMediaRouterCallbackImpl( + this.mergedSelector.asBundle(), + new MediaRouterCallbackImpl(this) + ); + + this.router.addCallback( + this.mergedSelector.asBundle(), + MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY + ); + } catch (RemoteException e) { + Log.w(TAG, "Failed to register media router callback: " + e.getMessage()); + } } @Override @@ -118,7 +149,6 @@ public void destroy() throws RemoteException { @Override public void onActivityResumed(IObjectWrapper activity) throws RemoteException { Log.d(TAG, "unimplemented Method: onActivityResumed"); - } @Override @@ -151,4 +181,4 @@ public CastOptions getOptions() { public IObjectWrapper getWrappedThis() throws RemoteException { return ObjectWrapper.wrap(this); } -} +} \ No newline at end of file diff --git a/play-services-cast-framework/src/main/AndroidManifest.xml b/play-services-cast-framework/src/main/AndroidManifest.xml index 4fddd28710..c2128ef028 100644 --- a/play-services-cast-framework/src/main/AndroidManifest.xml +++ b/play-services-cast-framework/src/main/AndroidManifest.xml @@ -8,23 +8,28 @@ + + + + + + android:name="com.google.android.gms.cast.framework.media.MediaIntentReceiver" + android:exported="false"/> + android:name="com.google.android.gms.cast.framework.media.MediaNotificationService" + android:exported="false" + android:foregroundServiceType="mediaPlayback"/> + android:name="com.google.android.gms.cast.framework.ReconnectionService" + android:exported="false"/> - + \ No newline at end of file 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..93657359ac 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 @@ -52,6 +52,7 @@ import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEvent; import su.litvak.chromecast.api.v2.ChromeCastRawMessage; import su.litvak.chromecast.api.v2.AppEvent; +import android.os.IBinder; public class CastDeviceControllerImpl extends ICastDeviceController.Stub implements ChromeCastConnectionEventListener, @@ -66,7 +67,9 @@ public class CastDeviceControllerImpl extends ICastDeviceController.Stub impleme private CastDevice castDevice; boolean notificationEnabled; long castFlags; - ICastDeviceControllerListener listener; + volatile ICastDeviceControllerListener listener; + private IBinder listenerBinder; + private IBinder.DeathRecipient listenerDeathRecipient; ChromeCast chromecast; @@ -82,7 +85,7 @@ public CastDeviceControllerImpl(Context context, String packageName, Bundle extr this.castFlags = extras.getLong("com.google.android.gms.cast.EXTRA_CAST_FLAGS"); BinderWrapper listenerWrapper = (BinderWrapper)extras.get("listener"); if (listenerWrapper != null) { - this.listener = ICastDeviceControllerListener.Stub.asInterface(listenerWrapper.binder); + replaceListener(ICastDeviceControllerListener.Stub.asInterface(listenerWrapper.binder)); } this.chromecast = new ChromeCast(this.castDevice.getAddress()); @@ -91,6 +94,55 @@ public CastDeviceControllerImpl(Context context, String packageName, Bundle extr this.chromecast.registerConnectionListener(this); } + private synchronized void replaceListener(ICastDeviceControllerListener newListener) { + // Unlink old death recipient before replacing + if (this.listenerBinder != null && this.listenerDeathRecipient != null) { + try { + this.listenerBinder.unlinkToDeath(this.listenerDeathRecipient, 0); + } catch (Exception ignored) {} + } + this.listener = null; + this.listenerBinder = null; + this.listenerDeathRecipient = null; + + if (newListener == null) return; + + IBinder binder = newListener.asBinder(); + IBinder.DeathRecipient dr = () -> handleListenerDeath(binder); + this.listener = newListener; + this.listenerBinder = binder; + this.listenerDeathRecipient = dr; + try { + binder.linkToDeath(dr, 0); + } catch (RemoteException e) { + Log.w(TAG, "Failed to link Cast client death: " + e.getMessage()); + handleListenerDeath(binder); + } + } + + private synchronized void clearListener() { + replaceListener(null); + } + + private void handleListenerDeath(IBinder deadBinder) { + synchronized (this) { + if (deadBinder != this.listenerBinder) return; + this.listener = null; + this.listenerBinder = null; + this.listenerDeathRecipient = null; + } + Log.d(TAG, "Cast client died; disconnecting"); + disconnectChromecast(); + } + + private void disconnectChromecast() { + try { + this.chromecast.disconnect(); + } catch (IOException e) { + Log.e(TAG, "Error disconnecting chromecast: " + e.getMessage()); + } + } + @Override public void connectionEventReceived(ChromeCastConnectionEvent event) { if (!event.isConnected()) { @@ -147,12 +199,7 @@ public void rawMessageReceived(ChromeCastRawMessage message, Long requestId) { switch (message.getPayloadType()) { case STRING: String response = message.getPayloadUtf8(); - if (requestId == null) { - this.onTextMessageReceived(message.getNamespace(), response); - } else { - this.onSendMessageSuccess(response, requestId); - this.onTextMessageReceived(message.getNamespace(), response); - } + this.onTextMessageReceived(message.getNamespace(), response); break; case BINARY: byte[] payload = message.getPayloadBinary(); @@ -163,14 +210,36 @@ public void rawMessageReceived(ChromeCastRawMessage message, Long requestId) { @Override public void disconnect() { + disconnectChromecast(); + clearListener(); + } + + @Override + public void connect() { + Log.d(TAG, "connect()"); try { - this.chromecast.disconnect(); - } catch (IOException e) { - Log.e(TAG, "Error disconnecting chromecast: " + e.getMessage()); - return; + if (!this.chromecast.isConnected()) { + this.chromecast.connect(); + } + this.onConnectedWithResult(CommonStatusCodes.SUCCESS); + } catch (Exception e) { + Log.w(TAG, "Error connecting to chromecast: " + e.getMessage()); + this.onConnectedWithResult(CommonStatusCodes.NETWORK_ERROR); } } + @Override + public void setListener(ICastDeviceControllerListener listener) { + Log.d(TAG, "setListener()"); + replaceListener(listener); + } + + @Override + public void unregisterListener() { + Log.d(TAG, "unregisterListener()"); + clearListener(); + } + @Override public void sendMessage(String namespace, String message, long requestId) { try { @@ -325,4 +394,15 @@ public void onDeviceStatusChanged(CastDeviceStatus deviceStatus) { } } } + + public void onConnectedWithResult(int statusCode) { + ICastDeviceControllerListener l = this.listener; + if (l != null) { + try { + l.onConnectedWithResult(statusCode); + } catch (RemoteException ex) { + Log.e(TAG, "Error calling onConnectedWithResult: " + 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..7efa78ac8f 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,38 @@ 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.Feature; +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.internal.ConnectionInfo; 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(); + private static final Feature[] FEATURES = new Feature[] { + new Feature("cxless_connect", 1L), + new Feature("cxless_set_listener", 1L) + }; + 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); + ConnectionInfo connectionInfo = new ConnectionInfo(); + connectionInfo.features = FEATURES; + + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + new CastDeviceControllerImpl(this, request.packageName, request.extras), + connectionInfo + ); } -} +} \ No newline at end of file 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..6114909694 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 @@ -1,5 +1,6 @@ package com.google.android.gms.cast.internal; +import com.google.android.gms.cast.internal.ICastDeviceControllerListener; import com.google.android.gms.cast.LaunchOptions; import com.google.android.gms.cast.JoinOptions; @@ -11,4 +12,7 @@ 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 setListener(ICastDeviceControllerListener listener) = 17; + oneway void unregisterListener() = 18; +} \ No newline at end of file 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..d48b695e5d 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 @@ -18,4 +18,5 @@ interface ICastDeviceControllerListener { void onSendMessageSuccess(String response, long requestId) = 10; void onApplicationStatusChanged(in ApplicationStatus applicationStatus) = 11; void onDeviceStatusChanged(in CastDeviceStatus deviceStatus) = 12; + void onConnectedWithResult(int statusCode) = 13; } diff --git a/play-services-cast/src/main/java/com/google/android/gms/cast/CastDevice.java b/play-services-cast/src/main/java/com/google/android/gms/cast/CastDevice.java index f185c8a5b8..0e5f00800f 100644 --- a/play-services-cast/src/main/java/com/google/android/gms/cast/CastDevice.java +++ b/play-services-cast/src/main/java/com/google/android/gms/cast/CastDevice.java @@ -48,7 +48,9 @@ public CastDevice ( this.deviceVersion = deviceVersion; this.friendlyName = friendlyName; this.icons = new ArrayList(); - this.icons.add(new WebImage(Uri.parse(String.format("http://%s:8008%s", this.address, iconPath)))); + this.icons.add(new WebImage( + Uri.parse(String.format("http://%s:8008%s", this.address, iconPath)) + )); this.modelName = modelName; this.capabilities = capabilities; }