Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 399f8800 authored by root's avatar root Committed by Jonathan Klee
Browse files

Cast: wire MediaRouter, refresh MDX callbacks, and load Conscrypt from GmsCore APK

parent 6ca63e68
Loading
Loading
Loading
Loading
+140 −6
Original line number Diff line number Diff line
// 2026 Murena SAS
/*
 * Copyright (C) 2013-2017 microG Project Team
 *
@@ -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;
@@ -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
@@ -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;
    }

@@ -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");
@@ -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() {
+1 −0
Original line number Diff line number Diff line
// 2026 Murena SAS
/*
 * Copyright (C) 2013-2017 microG Project Team
 *
+180 −4
Original line number Diff line number Diff line
// 2026 Murena SAS
/*
 * Copyright (C) 2013-2017 microG Project Team
 *
@@ -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;
@@ -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);
    }

@@ -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();
    }
}
+31 −2
Original line number Diff line number Diff line
@@ -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;

@@ -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
@@ -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());
            }
        }
    }
}
+5 −11
Original line number Diff line number Diff line
@@ -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) {
@@ -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