Loading play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java +140 −6 Original line number Diff line number Diff line // 2026 Murena SAS /* * Copyright (C) 2013-2017 microG Project Team * Loading @@ -16,10 +17,14 @@ package com.google.android.gms.cast.framework.internal; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.Build; import android.util.Log; import androidx.mediarouter.media.MediaControlIntent; Loading @@ -34,43 +39,126 @@ import com.google.android.gms.cast.framework.ISessionManager; import com.google.android.gms.cast.framework.ISessionProvider; import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.dynamic.ObjectWrapper; import com.google.android.gms.cast.framework.internal.MediaRouterImpl; import java.util.Map; import java.util.HashMap; public class CastContextImpl extends ICastContext.Stub { private static final String TAG = CastContextImpl.class.getSimpleName(); private static volatile CastContextImpl lastInstance; private static String pendingRouteId; private static Bundle pendingRouteExtras; private static final String ROUTE_SELECTED_ACTION = "com.google.android.gms.cast.framework.ROUTE_SELECTED"; private SessionManagerImpl sessionManager; private DiscoveryManagerImpl discoveryManager; private BroadcastReceiver routeSelectionReceiver; private Context context; private CastOptions options; private IMediaRouter router; private Map<String, ISessionProvider> sessionProviders = new HashMap<String, ISessionProvider>(); public ISessionProvider defaultSessionProvider; private String defaultCategory; private MediaRouteSelector mergedSelector; private MediaRouterCallbackImpl mediaRouterCallback; public CastContextImpl(IObjectWrapper context, CastOptions options, IMediaRouter router, Map<String, IBinder> sessionProviders) throws RemoteException { this.context = (Context) ObjectWrapper.unwrap(context); this.options = options; this.router = router; this.router = router != null ? router : new MediaRouterImpl(this.context); lastInstance = this; for (Map.Entry<String, IBinder> entry : sessionProviders.entrySet()) { this.sessionProviders.put(entry.getKey(), ISessionProvider.Stub.asInterface(entry.getValue())); String key = entry.getKey(); ISessionProvider provider = ISessionProvider.Stub.asInterface(entry.getValue()); this.sessionProviders.put(key, provider); String normalized = normalizeCategoryKey(key); if (!normalized.equals(key) && !this.sessionProviders.containsKey(normalized)) { this.sessionProviders.put(normalized, provider); } } String receiverApplicationId = options.getReceiverApplicationId(); String defaultCategory = CastMediaControlIntent.categoryForCast(receiverApplicationId); this.defaultCategory = CastMediaControlIntent.categoryForCast(receiverApplicationId); this.defaultSessionProvider = this.sessionProviders.get(defaultCategory); this.defaultSessionProvider = resolveSessionProviderForCategory(this.defaultCategory); // TODO: This should incorporate passed options this.mergedSelector = new MediaRouteSelector.Builder() .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) .addControlCategory(defaultCategory) .addControlCategory(this.defaultCategory) .build(); this.mediaRouterCallback = new MediaRouterCallbackImpl(this); this.router.registerMediaRouterCallbackImpl(this.mergedSelector.asBundle(), this.mediaRouterCallback); registerRouteSelectionReceiver(); } public static CastContextImpl getLastInstance() { return lastInstance; } public static void recordPendingRouteSelection(String routeId, Bundle extras) { pendingRouteId = routeId; pendingRouteExtras = extras; } public static void clearPendingRouteSelection() { pendingRouteId = null; pendingRouteExtras = null; } public static String getPendingRouteId() { return pendingRouteId; } public static Bundle getPendingRouteExtras() { return pendingRouteExtras; } private void registerRouteSelectionReceiver() { if (routeSelectionReceiver != null) { return; } routeSelectionReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent == null || !ROUTE_SELECTED_ACTION.equals(intent.getAction())) { return; } String routeId = intent.getStringExtra("route_id"); Bundle extras = intent.getBundleExtra("route_extras"); Log.d(TAG, "route selected broadcast routeId=" + routeId + " extras=" + summarizeExtras(extras)); recordPendingRouteSelection(routeId, extras); getSessionManagerImpl(); } }; try { IntentFilter filter = new IntentFilter(ROUTE_SELECTED_ACTION); if (Build.VERSION.SDK_INT >= 33) { context.registerReceiver(routeSelectionReceiver, filter, Context.RECEIVER_EXPORTED); } else { context.registerReceiver(routeSelectionReceiver, filter); } Log.d(TAG, "route selection receiver registered"); } catch (Exception e) { Log.d(TAG, "route selection receiver register failed: " + e.getMessage()); } } private static String summarizeExtras(Bundle extras) { if (extras == null) { return "null"; } try { return "keys=" + extras.keySet(); } catch (Exception e) { return "error:" + e.getMessage(); } } @Override Loading Loading @@ -99,6 +187,10 @@ public class CastContextImpl extends ICastContext.Stub { if (this.sessionManager == null) { this.sessionManager = new SessionManagerImpl(this); } if (pendingRouteId != null) { this.sessionManager.onRouteSelected(pendingRouteId, pendingRouteExtras); clearPendingRouteSelection(); } return this.sessionManager; } Loading @@ -110,6 +202,41 @@ public class CastContextImpl extends ICastContext.Stub { return this.discoveryManager; } public String getDefaultCategory() { return this.defaultCategory; } public ISessionProvider resolveSessionProviderForCategory(String category) { if (category == null) { return null; } ISessionProvider provider = this.sessionProviders.get(category); if (provider != null) { return provider; } String normalized = normalizeCategoryKey(category); provider = this.sessionProviders.get(normalized); if (provider != null) { return provider; } String prefix = normalized + "///"; for (Map.Entry<String, ISessionProvider> entry : this.sessionProviders.entrySet()) { String key = entry.getKey(); if (key != null && (key.startsWith(prefix) || key.startsWith(category + "///"))) { return entry.getValue(); } } return null; } private static String normalizeCategoryKey(String key) { if (key == null) { return null; } int idx = key.indexOf("///"); return idx >= 0 ? key.substring(0, idx) : key; } @Override public void destroy() throws RemoteException { Log.d(TAG, "unimplemented Method: destroy"); Loading @@ -128,7 +255,14 @@ public class CastContextImpl extends ICastContext.Stub { @Override public void setReceiverApplicationId(String receiverApplicationId, Map sessionProvidersByCategory) throws RemoteException { Log.d(TAG, "unimplemented Method: setReceiverApplicationId"); this.defaultCategory = CastMediaControlIntent.categoryForCast(receiverApplicationId); this.defaultSessionProvider = resolveSessionProviderForCategory(this.defaultCategory); this.mergedSelector = new MediaRouteSelector.Builder() .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) .addControlCategory(this.defaultCategory) .build(); this.router.registerMediaRouterCallbackImpl(this.mergedSelector.asBundle(), this.mediaRouterCallback); } public Context getContext() { Loading play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/CastDynamiteModuleImpl.java +1 −0 Original line number Diff line number Diff line // 2026 Murena SAS /* * Copyright (C) 2013-2017 microG Project Team * Loading play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/CastSessionImpl.java +180 −4 Original line number Diff line number Diff line // 2026 Murena SAS /* * Copyright (C) 2013-2017 microG Project Team * Loading @@ -23,17 +24,30 @@ import android.os.RemoteException; import android.util.Log; import com.google.android.gms.cast.ApplicationMetadata; import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.LaunchOptions; import com.google.android.gms.cast.framework.CastOptions; import com.google.android.gms.cast.framework.ICastConnectionController; import com.google.android.gms.common.api.Status; import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.dynamic.ObjectWrapper; import java.util.List; import java.util.regex.Pattern; public class CastSessionImpl extends ICastSession.Stub { private static final String TAG = CastSessionImpl.class.getSimpleName(); private static final Pattern CAST_APP_ID_PATTERN = Pattern.compile("^[0-9A-Fa-f]{8}$"); private static final String YOUTUBE_APP_ID_PRIMARY = "233637DE"; private static final String YOUTUBE_APP_ID_FALLBACK = "0F5096E8"; private CastOptions options; private SessionImpl session; private ICastConnectionController controller; private String lastLaunchAppId; private LaunchOptions lastLaunchOptions; private boolean youtubeFallbackAttempted; private boolean appConnectionSucceeded; public CastSessionImpl(CastOptions options, IObjectWrapper session, ICastConnectionController controller) throws RemoteException { this.options = options; Loading @@ -44,26 +58,51 @@ public class CastSessionImpl extends ICastSession.Stub { } public void launchApplication() throws RemoteException { this.controller.launchApplication(this.options.getReceiverApplicationId(), this.options.getLaunchOptions()); String appId = getReceiverApplicationIdSafe(); LaunchOptions opts = getLaunchOptionsSafe(); rememberLaunch(appId, opts); Log.d(TAG, "launchApplication appId=" + appId + " opts=" + opts); this.controller.launchApplication(appId, opts); } @Override public void onConnected(Bundle routeInfoExtra) throws RemoteException { this.controller.launchApplication(this.options.getReceiverApplicationId(), this.options.getLaunchOptions()); String appId = getReceiverApplicationIdSafe(); LaunchOptions opts = getLaunchOptionsSafe(); rememberLaunch(appId, opts); Log.d(TAG, "onConnected launch appId=" + appId + " opts=" + opts); this.controller.launchApplication(appId, opts); } @Override public void onConnectionSuspended(int reason) { Log.d(TAG, "unimplemented Method: onConnectionSuspended"); Log.d(TAG, "onConnectionSuspended reason=" + reason); } @Override public void onConnectionFailed(Status status) { Log.d(TAG, "unimplemented Method: onConnectionFailed"); Log.w(TAG, "onConnectionFailed " + formatStatus(status) + " appConnected=" + appConnectionSucceeded); if (!appConnectionSucceeded && shouldAttemptYouTubeFallback(status)) { try { youtubeFallbackAttempted = true; Log.w(TAG, "retry launch with YouTube fallback appId=" + YOUTUBE_APP_ID_FALLBACK + " appConnected=" + appConnectionSucceeded); this.controller.launchApplication(YOUTUBE_APP_ID_FALLBACK, safeLastLaunchOptions()); } catch (RemoteException e) { Log.e(TAG, "fallback launch failed: " + e.getMessage(), e); } return; } if (appConnectionSucceeded) { Log.w(TAG, "onConnectionFailed after app connection success; ignoring fallback"); return; } } @Override public void onApplicationConnectionSuccess(ApplicationMetadata applicationMetadata, String applicationStatus, String sessionId, boolean wasLaunched) { appConnectionSucceeded = true; Log.d(TAG, "onApplicationConnectionSuccess sessionId=" + sessionId + " wasLaunched=" + wasLaunched); this.session.onApplicationConnectionSuccess(applicationMetadata, applicationStatus, sessionId, wasLaunched); } Loading @@ -76,4 +115,141 @@ public class CastSessionImpl extends ICastSession.Stub { public void disconnectFromDevice(boolean boolean1, int int1) { Log.d(TAG, "unimplemented Method: disconnectFromDevice"); } private static String formatStatus(Status status) { if (status == null) { return "status=null"; } int code = status.getStatusCode(); String codeLabel = CommonStatusCodes.getStatusCodeString(code); String message = status.getStatusMessage(); boolean hasResolution = status.hasResolution(); return "statusCode=" + code + " (" + codeLabel + ")" + " message=" + message + " hasResolution=" + hasResolution; } private String getReceiverApplicationIdSafe() { if (options == null) { Log.d(TAG, "receiver appId fallback: options=null; using DEFAULT"); return CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; } try { String appId = options.getReceiverApplicationId(); Log.d(TAG, "receiver appId from CastOptions: " + appId); return appId; } catch (NoSuchMethodError error) { // Handle app-bundled CastOptions without getReceiverApplicationId (YouTube). Log.w(TAG, "CastOptions.getReceiverApplicationId missing; attempting reflection"); } catch (Exception e) { Log.w(TAG, "CastOptions.getReceiverApplicationId failed; attempting reflection " + e.getMessage()); } try { java.lang.reflect.Field field = options.getClass().getDeclaredField("receiverApplicationId"); field.setAccessible(true); Object value = field.get(options); if (value instanceof String) { String appId = (String) value; Log.d(TAG, "receiver appId from field receiverApplicationId: " + appId); return appId; } } catch (Exception ignored) { } String fallback = readReceiverApplicationIdFromFields(options); if (fallback != null) { Log.d(TAG, "receiver appId from reflection scan: " + fallback); return fallback; } Log.d(TAG, "receiver appId fallback: using DEFAULT"); return CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; } private String readReceiverApplicationIdFromFields(Object source) { String fallback = null; Class<?> cursor = source.getClass(); while (cursor != null && cursor != Object.class) { for (java.lang.reflect.Field field : cursor.getDeclaredFields()) { try { field.setAccessible(true); Object value = field.get(source); if (value instanceof String) { String str = (String) value; if (CAST_APP_ID_PATTERN.matcher(str).matches()) { if (!CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID.equals(str)) { return str; } fallback = str; } } else if (value instanceof List) { for (Object entry : (List<?>) value) { if (entry instanceof String) { String str = (String) entry; if (CAST_APP_ID_PATTERN.matcher(str).matches()) { if (!CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID.equals(str)) { return str; } fallback = str; } } } } } catch (Exception ignored) { } } cursor = cursor.getSuperclass(); } return fallback; } private LaunchOptions getLaunchOptionsSafe() { if (options == null) { return new LaunchOptions(); } try { LaunchOptions opts = options.getLaunchOptions(); return opts != null ? opts : new LaunchOptions(); } catch (NoSuchMethodError error) { // Older CastOptions bundles omit getLaunchOptions; pull the field directly. Log.w(TAG, "CastOptions.getLaunchOptions missing; attempting reflection"); } catch (Exception e) { Log.w(TAG, "CastOptions.getLaunchOptions failed; attempting reflection " + e.getMessage()); } try { java.lang.reflect.Field field = options.getClass().getDeclaredField("launchOptions"); field.setAccessible(true); Object value = field.get(options); if (value instanceof LaunchOptions) { return (LaunchOptions) value; } } catch (Exception ignored) { } return new LaunchOptions(); } private void rememberLaunch(String appId, LaunchOptions opts) { this.lastLaunchAppId = appId; this.lastLaunchOptions = opts; this.youtubeFallbackAttempted = false; } private boolean shouldAttemptYouTubeFallback(Status status) { if (youtubeFallbackAttempted) { return false; } if (!YOUTUBE_APP_ID_PRIMARY.equals(lastLaunchAppId)) { return false; } if (status == null) { return false; } // Only retry when the failure maps to "service missing", which currently blocks YouTube. return status.getStatusCode() == CommonStatusCodes.SERVICE_MISSING; } private LaunchOptions safeLastLaunchOptions() { if (lastLaunchOptions != null) { return lastLaunchOptions; } return new LaunchOptions(); } } play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/DiscoveryManagerImpl.java +31 −2 Original line number Diff line number Diff line Loading @@ -25,6 +25,8 @@ import com.google.android.gms.cast.framework.internal.CastContextImpl; import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.dynamic.ObjectWrapper; import androidx.mediarouter.media.MediaRouter; import java.util.Set; import java.util.HashSet; Loading @@ -41,12 +43,23 @@ public class DiscoveryManagerImpl extends IDiscoveryManager.Stub { @Override public void startDiscovery() { Log.d(TAG, "unimplemented Method: startDiscovery"); try { castContextImpl.getRouter().addCallback(castContextImpl.getMergedSelector().asBundle(), MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); notifyDeviceAvailabilityChanged(isDeviceAvailable()); } catch (RemoteException e) { Log.d(TAG, "Remote exception calling startDiscovery: " + e.getMessage()); } } @Override public void stopDiscovery() { Log.d(TAG, "unimplemented Method: stopDiscovery"); try { castContextImpl.getRouter().removeCallback(castContextImpl.getMergedSelector().asBundle()); notifyDeviceAvailabilityChanged(isDeviceAvailable()); } catch (RemoteException e) { Log.d(TAG, "Remote exception calling stopDiscovery: " + e.getMessage()); } } @Override Loading @@ -65,4 +78,20 @@ public class DiscoveryManagerImpl extends IDiscoveryManager.Stub { public IObjectWrapper getWrappedThis() throws RemoteException { return ObjectWrapper.wrap(this); } private boolean isDeviceAvailable() throws RemoteException { return castContextImpl.getRouter().isRouteAvailable(castContextImpl.getMergedSelector().asBundle(), MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); } private void notifyDeviceAvailabilityChanged(boolean available) { for (Object listenerObj : discoveryManagerListeners) { IDiscoveryManagerListener listener = (IDiscoveryManagerListener) listenerObj; try { listener.onDeviceAvailabilityChanged(available); } catch (RemoteException e) { Log.d(TAG, "Remote exception calling onDeviceAvailabilityChanged: " + e.getMessage()); } } } } play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java +5 −11 Original line number Diff line number Diff line Loading @@ -37,25 +37,19 @@ public class MediaRouterCallbackImpl extends IMediaRouterCallback.Stub { @Override public void onRouteAdded(String routeId, Bundle extras) { Log.d(TAG, "unimplemented Method: onRouteAdded"); Log.d(TAG, "onRouteAdded: " + routeId); } @Override public void onRouteChanged(String routeId, Bundle extras) { Log.d(TAG, "unimplemented Method: onRouteChanged"); Log.d(TAG, "onRouteChanged: " + routeId); } @Override public void onRouteRemoved(String routeId, Bundle extras) { Log.d(TAG, "unimplemented Method: onRouteRemoved"); Log.d(TAG, "onRouteRemoved: " + routeId); } @Override public void onRouteSelected(String routeId, Bundle extras) throws RemoteException { CastDevice castDevice = CastDevice.getFromBundle(extras); SessionImpl session = (SessionImpl) ObjectWrapper.unwrap(this.castContext.defaultSessionProvider.getSession(null)); Bundle routeInfoExtras = this.castContext.getRouter().getRouteInfoExtrasById(routeId); if (routeInfoExtras != null) { session.start(this.castContext, castDevice, routeId, routeInfoExtras); } this.castContext.getSessionManagerImpl().onRouteSelected(routeId, extras); } @Override public void unknown(String routeId, Bundle extras) { Loading @@ -63,6 +57,6 @@ public class MediaRouterCallbackImpl extends IMediaRouterCallback.Stub { } @Override public void onRouteUnselected(String routeId, Bundle extras, int reason) { Log.d(TAG, "unimplemented Method: onRouteUnselected"); Log.d(TAG, "onRouteUnselected: " + routeId + " reason=" + reason); } } Loading
play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java +140 −6 Original line number Diff line number Diff line // 2026 Murena SAS /* * Copyright (C) 2013-2017 microG Project Team * Loading @@ -16,10 +17,14 @@ package com.google.android.gms.cast.framework.internal; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.Build; import android.util.Log; import androidx.mediarouter.media.MediaControlIntent; Loading @@ -34,43 +39,126 @@ import com.google.android.gms.cast.framework.ISessionManager; import com.google.android.gms.cast.framework.ISessionProvider; import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.dynamic.ObjectWrapper; import com.google.android.gms.cast.framework.internal.MediaRouterImpl; import java.util.Map; import java.util.HashMap; public class CastContextImpl extends ICastContext.Stub { private static final String TAG = CastContextImpl.class.getSimpleName(); private static volatile CastContextImpl lastInstance; private static String pendingRouteId; private static Bundle pendingRouteExtras; private static final String ROUTE_SELECTED_ACTION = "com.google.android.gms.cast.framework.ROUTE_SELECTED"; private SessionManagerImpl sessionManager; private DiscoveryManagerImpl discoveryManager; private BroadcastReceiver routeSelectionReceiver; private Context context; private CastOptions options; private IMediaRouter router; private Map<String, ISessionProvider> sessionProviders = new HashMap<String, ISessionProvider>(); public ISessionProvider defaultSessionProvider; private String defaultCategory; private MediaRouteSelector mergedSelector; private MediaRouterCallbackImpl mediaRouterCallback; public CastContextImpl(IObjectWrapper context, CastOptions options, IMediaRouter router, Map<String, IBinder> sessionProviders) throws RemoteException { this.context = (Context) ObjectWrapper.unwrap(context); this.options = options; this.router = router; this.router = router != null ? router : new MediaRouterImpl(this.context); lastInstance = this; for (Map.Entry<String, IBinder> entry : sessionProviders.entrySet()) { this.sessionProviders.put(entry.getKey(), ISessionProvider.Stub.asInterface(entry.getValue())); String key = entry.getKey(); ISessionProvider provider = ISessionProvider.Stub.asInterface(entry.getValue()); this.sessionProviders.put(key, provider); String normalized = normalizeCategoryKey(key); if (!normalized.equals(key) && !this.sessionProviders.containsKey(normalized)) { this.sessionProviders.put(normalized, provider); } } String receiverApplicationId = options.getReceiverApplicationId(); String defaultCategory = CastMediaControlIntent.categoryForCast(receiverApplicationId); this.defaultCategory = CastMediaControlIntent.categoryForCast(receiverApplicationId); this.defaultSessionProvider = this.sessionProviders.get(defaultCategory); this.defaultSessionProvider = resolveSessionProviderForCategory(this.defaultCategory); // TODO: This should incorporate passed options this.mergedSelector = new MediaRouteSelector.Builder() .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) .addControlCategory(defaultCategory) .addControlCategory(this.defaultCategory) .build(); this.mediaRouterCallback = new MediaRouterCallbackImpl(this); this.router.registerMediaRouterCallbackImpl(this.mergedSelector.asBundle(), this.mediaRouterCallback); registerRouteSelectionReceiver(); } public static CastContextImpl getLastInstance() { return lastInstance; } public static void recordPendingRouteSelection(String routeId, Bundle extras) { pendingRouteId = routeId; pendingRouteExtras = extras; } public static void clearPendingRouteSelection() { pendingRouteId = null; pendingRouteExtras = null; } public static String getPendingRouteId() { return pendingRouteId; } public static Bundle getPendingRouteExtras() { return pendingRouteExtras; } private void registerRouteSelectionReceiver() { if (routeSelectionReceiver != null) { return; } routeSelectionReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent == null || !ROUTE_SELECTED_ACTION.equals(intent.getAction())) { return; } String routeId = intent.getStringExtra("route_id"); Bundle extras = intent.getBundleExtra("route_extras"); Log.d(TAG, "route selected broadcast routeId=" + routeId + " extras=" + summarizeExtras(extras)); recordPendingRouteSelection(routeId, extras); getSessionManagerImpl(); } }; try { IntentFilter filter = new IntentFilter(ROUTE_SELECTED_ACTION); if (Build.VERSION.SDK_INT >= 33) { context.registerReceiver(routeSelectionReceiver, filter, Context.RECEIVER_EXPORTED); } else { context.registerReceiver(routeSelectionReceiver, filter); } Log.d(TAG, "route selection receiver registered"); } catch (Exception e) { Log.d(TAG, "route selection receiver register failed: " + e.getMessage()); } } private static String summarizeExtras(Bundle extras) { if (extras == null) { return "null"; } try { return "keys=" + extras.keySet(); } catch (Exception e) { return "error:" + e.getMessage(); } } @Override Loading Loading @@ -99,6 +187,10 @@ public class CastContextImpl extends ICastContext.Stub { if (this.sessionManager == null) { this.sessionManager = new SessionManagerImpl(this); } if (pendingRouteId != null) { this.sessionManager.onRouteSelected(pendingRouteId, pendingRouteExtras); clearPendingRouteSelection(); } return this.sessionManager; } Loading @@ -110,6 +202,41 @@ public class CastContextImpl extends ICastContext.Stub { return this.discoveryManager; } public String getDefaultCategory() { return this.defaultCategory; } public ISessionProvider resolveSessionProviderForCategory(String category) { if (category == null) { return null; } ISessionProvider provider = this.sessionProviders.get(category); if (provider != null) { return provider; } String normalized = normalizeCategoryKey(category); provider = this.sessionProviders.get(normalized); if (provider != null) { return provider; } String prefix = normalized + "///"; for (Map.Entry<String, ISessionProvider> entry : this.sessionProviders.entrySet()) { String key = entry.getKey(); if (key != null && (key.startsWith(prefix) || key.startsWith(category + "///"))) { return entry.getValue(); } } return null; } private static String normalizeCategoryKey(String key) { if (key == null) { return null; } int idx = key.indexOf("///"); return idx >= 0 ? key.substring(0, idx) : key; } @Override public void destroy() throws RemoteException { Log.d(TAG, "unimplemented Method: destroy"); Loading @@ -128,7 +255,14 @@ public class CastContextImpl extends ICastContext.Stub { @Override public void setReceiverApplicationId(String receiverApplicationId, Map sessionProvidersByCategory) throws RemoteException { Log.d(TAG, "unimplemented Method: setReceiverApplicationId"); this.defaultCategory = CastMediaControlIntent.categoryForCast(receiverApplicationId); this.defaultSessionProvider = resolveSessionProviderForCategory(this.defaultCategory); this.mergedSelector = new MediaRouteSelector.Builder() .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) .addControlCategory(this.defaultCategory) .build(); this.router.registerMediaRouterCallbackImpl(this.mergedSelector.asBundle(), this.mediaRouterCallback); } public Context getContext() { Loading
play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/CastDynamiteModuleImpl.java +1 −0 Original line number Diff line number Diff line // 2026 Murena SAS /* * Copyright (C) 2013-2017 microG Project Team * Loading
play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/CastSessionImpl.java +180 −4 Original line number Diff line number Diff line // 2026 Murena SAS /* * Copyright (C) 2013-2017 microG Project Team * Loading @@ -23,17 +24,30 @@ import android.os.RemoteException; import android.util.Log; import com.google.android.gms.cast.ApplicationMetadata; import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.LaunchOptions; import com.google.android.gms.cast.framework.CastOptions; import com.google.android.gms.cast.framework.ICastConnectionController; import com.google.android.gms.common.api.Status; import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.dynamic.ObjectWrapper; import java.util.List; import java.util.regex.Pattern; public class CastSessionImpl extends ICastSession.Stub { private static final String TAG = CastSessionImpl.class.getSimpleName(); private static final Pattern CAST_APP_ID_PATTERN = Pattern.compile("^[0-9A-Fa-f]{8}$"); private static final String YOUTUBE_APP_ID_PRIMARY = "233637DE"; private static final String YOUTUBE_APP_ID_FALLBACK = "0F5096E8"; private CastOptions options; private SessionImpl session; private ICastConnectionController controller; private String lastLaunchAppId; private LaunchOptions lastLaunchOptions; private boolean youtubeFallbackAttempted; private boolean appConnectionSucceeded; public CastSessionImpl(CastOptions options, IObjectWrapper session, ICastConnectionController controller) throws RemoteException { this.options = options; Loading @@ -44,26 +58,51 @@ public class CastSessionImpl extends ICastSession.Stub { } public void launchApplication() throws RemoteException { this.controller.launchApplication(this.options.getReceiverApplicationId(), this.options.getLaunchOptions()); String appId = getReceiverApplicationIdSafe(); LaunchOptions opts = getLaunchOptionsSafe(); rememberLaunch(appId, opts); Log.d(TAG, "launchApplication appId=" + appId + " opts=" + opts); this.controller.launchApplication(appId, opts); } @Override public void onConnected(Bundle routeInfoExtra) throws RemoteException { this.controller.launchApplication(this.options.getReceiverApplicationId(), this.options.getLaunchOptions()); String appId = getReceiverApplicationIdSafe(); LaunchOptions opts = getLaunchOptionsSafe(); rememberLaunch(appId, opts); Log.d(TAG, "onConnected launch appId=" + appId + " opts=" + opts); this.controller.launchApplication(appId, opts); } @Override public void onConnectionSuspended(int reason) { Log.d(TAG, "unimplemented Method: onConnectionSuspended"); Log.d(TAG, "onConnectionSuspended reason=" + reason); } @Override public void onConnectionFailed(Status status) { Log.d(TAG, "unimplemented Method: onConnectionFailed"); Log.w(TAG, "onConnectionFailed " + formatStatus(status) + " appConnected=" + appConnectionSucceeded); if (!appConnectionSucceeded && shouldAttemptYouTubeFallback(status)) { try { youtubeFallbackAttempted = true; Log.w(TAG, "retry launch with YouTube fallback appId=" + YOUTUBE_APP_ID_FALLBACK + " appConnected=" + appConnectionSucceeded); this.controller.launchApplication(YOUTUBE_APP_ID_FALLBACK, safeLastLaunchOptions()); } catch (RemoteException e) { Log.e(TAG, "fallback launch failed: " + e.getMessage(), e); } return; } if (appConnectionSucceeded) { Log.w(TAG, "onConnectionFailed after app connection success; ignoring fallback"); return; } } @Override public void onApplicationConnectionSuccess(ApplicationMetadata applicationMetadata, String applicationStatus, String sessionId, boolean wasLaunched) { appConnectionSucceeded = true; Log.d(TAG, "onApplicationConnectionSuccess sessionId=" + sessionId + " wasLaunched=" + wasLaunched); this.session.onApplicationConnectionSuccess(applicationMetadata, applicationStatus, sessionId, wasLaunched); } Loading @@ -76,4 +115,141 @@ public class CastSessionImpl extends ICastSession.Stub { public void disconnectFromDevice(boolean boolean1, int int1) { Log.d(TAG, "unimplemented Method: disconnectFromDevice"); } private static String formatStatus(Status status) { if (status == null) { return "status=null"; } int code = status.getStatusCode(); String codeLabel = CommonStatusCodes.getStatusCodeString(code); String message = status.getStatusMessage(); boolean hasResolution = status.hasResolution(); return "statusCode=" + code + " (" + codeLabel + ")" + " message=" + message + " hasResolution=" + hasResolution; } private String getReceiverApplicationIdSafe() { if (options == null) { Log.d(TAG, "receiver appId fallback: options=null; using DEFAULT"); return CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; } try { String appId = options.getReceiverApplicationId(); Log.d(TAG, "receiver appId from CastOptions: " + appId); return appId; } catch (NoSuchMethodError error) { // Handle app-bundled CastOptions without getReceiverApplicationId (YouTube). Log.w(TAG, "CastOptions.getReceiverApplicationId missing; attempting reflection"); } catch (Exception e) { Log.w(TAG, "CastOptions.getReceiverApplicationId failed; attempting reflection " + e.getMessage()); } try { java.lang.reflect.Field field = options.getClass().getDeclaredField("receiverApplicationId"); field.setAccessible(true); Object value = field.get(options); if (value instanceof String) { String appId = (String) value; Log.d(TAG, "receiver appId from field receiverApplicationId: " + appId); return appId; } } catch (Exception ignored) { } String fallback = readReceiverApplicationIdFromFields(options); if (fallback != null) { Log.d(TAG, "receiver appId from reflection scan: " + fallback); return fallback; } Log.d(TAG, "receiver appId fallback: using DEFAULT"); return CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; } private String readReceiverApplicationIdFromFields(Object source) { String fallback = null; Class<?> cursor = source.getClass(); while (cursor != null && cursor != Object.class) { for (java.lang.reflect.Field field : cursor.getDeclaredFields()) { try { field.setAccessible(true); Object value = field.get(source); if (value instanceof String) { String str = (String) value; if (CAST_APP_ID_PATTERN.matcher(str).matches()) { if (!CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID.equals(str)) { return str; } fallback = str; } } else if (value instanceof List) { for (Object entry : (List<?>) value) { if (entry instanceof String) { String str = (String) entry; if (CAST_APP_ID_PATTERN.matcher(str).matches()) { if (!CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID.equals(str)) { return str; } fallback = str; } } } } } catch (Exception ignored) { } } cursor = cursor.getSuperclass(); } return fallback; } private LaunchOptions getLaunchOptionsSafe() { if (options == null) { return new LaunchOptions(); } try { LaunchOptions opts = options.getLaunchOptions(); return opts != null ? opts : new LaunchOptions(); } catch (NoSuchMethodError error) { // Older CastOptions bundles omit getLaunchOptions; pull the field directly. Log.w(TAG, "CastOptions.getLaunchOptions missing; attempting reflection"); } catch (Exception e) { Log.w(TAG, "CastOptions.getLaunchOptions failed; attempting reflection " + e.getMessage()); } try { java.lang.reflect.Field field = options.getClass().getDeclaredField("launchOptions"); field.setAccessible(true); Object value = field.get(options); if (value instanceof LaunchOptions) { return (LaunchOptions) value; } } catch (Exception ignored) { } return new LaunchOptions(); } private void rememberLaunch(String appId, LaunchOptions opts) { this.lastLaunchAppId = appId; this.lastLaunchOptions = opts; this.youtubeFallbackAttempted = false; } private boolean shouldAttemptYouTubeFallback(Status status) { if (youtubeFallbackAttempted) { return false; } if (!YOUTUBE_APP_ID_PRIMARY.equals(lastLaunchAppId)) { return false; } if (status == null) { return false; } // Only retry when the failure maps to "service missing", which currently blocks YouTube. return status.getStatusCode() == CommonStatusCodes.SERVICE_MISSING; } private LaunchOptions safeLastLaunchOptions() { if (lastLaunchOptions != null) { return lastLaunchOptions; } return new LaunchOptions(); } }
play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/DiscoveryManagerImpl.java +31 −2 Original line number Diff line number Diff line Loading @@ -25,6 +25,8 @@ import com.google.android.gms.cast.framework.internal.CastContextImpl; import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.dynamic.ObjectWrapper; import androidx.mediarouter.media.MediaRouter; import java.util.Set; import java.util.HashSet; Loading @@ -41,12 +43,23 @@ public class DiscoveryManagerImpl extends IDiscoveryManager.Stub { @Override public void startDiscovery() { Log.d(TAG, "unimplemented Method: startDiscovery"); try { castContextImpl.getRouter().addCallback(castContextImpl.getMergedSelector().asBundle(), MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); notifyDeviceAvailabilityChanged(isDeviceAvailable()); } catch (RemoteException e) { Log.d(TAG, "Remote exception calling startDiscovery: " + e.getMessage()); } } @Override public void stopDiscovery() { Log.d(TAG, "unimplemented Method: stopDiscovery"); try { castContextImpl.getRouter().removeCallback(castContextImpl.getMergedSelector().asBundle()); notifyDeviceAvailabilityChanged(isDeviceAvailable()); } catch (RemoteException e) { Log.d(TAG, "Remote exception calling stopDiscovery: " + e.getMessage()); } } @Override Loading @@ -65,4 +78,20 @@ public class DiscoveryManagerImpl extends IDiscoveryManager.Stub { public IObjectWrapper getWrappedThis() throws RemoteException { return ObjectWrapper.wrap(this); } private boolean isDeviceAvailable() throws RemoteException { return castContextImpl.getRouter().isRouteAvailable(castContextImpl.getMergedSelector().asBundle(), MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); } private void notifyDeviceAvailabilityChanged(boolean available) { for (Object listenerObj : discoveryManagerListeners) { IDiscoveryManagerListener listener = (IDiscoveryManagerListener) listenerObj; try { listener.onDeviceAvailabilityChanged(available); } catch (RemoteException e) { Log.d(TAG, "Remote exception calling onDeviceAvailabilityChanged: " + e.getMessage()); } } } }
play-services-cast-framework/core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java +5 −11 Original line number Diff line number Diff line Loading @@ -37,25 +37,19 @@ public class MediaRouterCallbackImpl extends IMediaRouterCallback.Stub { @Override public void onRouteAdded(String routeId, Bundle extras) { Log.d(TAG, "unimplemented Method: onRouteAdded"); Log.d(TAG, "onRouteAdded: " + routeId); } @Override public void onRouteChanged(String routeId, Bundle extras) { Log.d(TAG, "unimplemented Method: onRouteChanged"); Log.d(TAG, "onRouteChanged: " + routeId); } @Override public void onRouteRemoved(String routeId, Bundle extras) { Log.d(TAG, "unimplemented Method: onRouteRemoved"); Log.d(TAG, "onRouteRemoved: " + routeId); } @Override public void onRouteSelected(String routeId, Bundle extras) throws RemoteException { CastDevice castDevice = CastDevice.getFromBundle(extras); SessionImpl session = (SessionImpl) ObjectWrapper.unwrap(this.castContext.defaultSessionProvider.getSession(null)); Bundle routeInfoExtras = this.castContext.getRouter().getRouteInfoExtrasById(routeId); if (routeInfoExtras != null) { session.start(this.castContext, castDevice, routeId, routeInfoExtras); } this.castContext.getSessionManagerImpl().onRouteSelected(routeId, extras); } @Override public void unknown(String routeId, Bundle extras) { Loading @@ -63,6 +57,6 @@ public class MediaRouterCallbackImpl extends IMediaRouterCallback.Stub { } @Override public void onRouteUnselected(String routeId, Bundle extras, int reason) { Log.d(TAG, "unimplemented Method: onRouteUnselected"); Log.d(TAG, "onRouteUnselected: " + routeId + " reason=" + reason); } }