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

Commit 18da6182 authored by Santiago Seifert's avatar Santiago Seifert
Browse files

Add media stream management in MR2ProviderService

Bug: b/362507305
Test: atest CtsMediaBetterTogetherTestCases CtsMediaHostTestCasts
Flag: com.android.media.flags.enable_mirroring_in_media_router_2
Change-Id: If1d82bf72a713201b5e83d4115104560c62ee803
parent 7879f67a
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
@@ -33,6 +33,23 @@ oneway interface IMediaRoute2ProviderService {

    void requestCreateSession(long requestId, String packageName, String routeId,
            in @nullable Bundle sessionHints);
    /**
     * Requests the creation of a system media routing session.
     *
     * @param requestId The id of the request.
     * @param uid The uid of the package whose media to route, or
     *     {@link android.os.Process#INVALID_UID} if routing should not be restricted to a specific
     *     uid.
     * @param packageName The name of the package whose media to route.
     * @param routeId The id of the route to be initially selected.
     * @param sessionHints An optional bundle with parameters.
     */
    void requestCreateSystemMediaSession(
            long requestId,
            int uid,
            String packageName,
            String routeId,
            in @nullable Bundle sessionHints);
    void selectRoute(long requestId, String sessionId, String routeId);
    void deselectRoute(long requestId, String sessionId, String routeId);
    void transferToRoute(long requestId, String sessionId, String routeId);
+243 −15
Original line number Diff line number Diff line
@@ -18,14 +18,21 @@ package android.media;

import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;

import static java.util.Objects.requireNonNull;

import android.Manifest;
import android.annotation.CallSuper;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.app.Service;
import android.content.Intent;
import android.media.audiopolicy.AudioMix;
import android.media.audiopolicy.AudioMixingRule;
import android.media.audiopolicy.AudioPolicy;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
@@ -36,6 +43,7 @@ import android.os.RemoteException;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.LongSparseArray;

import com.android.internal.annotations.GuardedBy;
import com.android.media.flags.Flags;
@@ -47,7 +55,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;

/**
@@ -83,8 +90,7 @@ public abstract class MediaRoute2ProviderService extends Service {
    public static final String SERVICE_INTERFACE = "android.media.MediaRoute2ProviderService";

    /**
     * {@link Intent} action that indicates that the declaring service supports routing of the
     * system media.
     * A category that indicates that the declaring service supports routing of the system media.
     *
     * <p>Providers must include this action if they intend to publish routes that support the
     * system media, as described by {@link MediaRoute2Info#getSupportedRoutingTypes()}.
@@ -94,7 +100,7 @@ public abstract class MediaRoute2ProviderService extends Service {
     */
    // TODO: b/362507305 - Unhide once the implementation and CTS are in place.
    @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
    @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
    @SdkConstant(SdkConstant.SdkConstantType.INTENT_CATEGORY)
    public static final String SERVICE_INTERFACE_SYSTEM_MEDIA =
            "android.media.MediaRoute2ProviderService.SYSTEM_MEDIA";

@@ -165,6 +171,16 @@ public abstract class MediaRoute2ProviderService extends Service {
    @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
    public static final int REASON_UNIMPLEMENTED = 5;

    /**
     * The request has failed because the provider has failed to route system media.
     *
     * @see #notifyRequestFailed
     * @hide
     */
    // TODO: b/362507305 - Unhide once the implementation and CTS are in place.
    @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
    public static final int REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA = 6;

    /** @hide */
    @IntDef(
            prefix = "REASON_",
@@ -174,7 +190,8 @@ public abstract class MediaRoute2ProviderService extends Service {
                REASON_NETWORK_ERROR,
                REASON_ROUTE_NOT_AVAILABLE,
                REASON_INVALID_COMMAND,
                REASON_UNIMPLEMENTED
                REASON_UNIMPLEMENTED,
                REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA
            })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Reason {}
@@ -187,15 +204,28 @@ public abstract class MediaRoute2ProviderService extends Service {
    private final AtomicBoolean mStatePublishScheduled = new AtomicBoolean(false);
    private final AtomicBoolean mSessionUpdateScheduled = new AtomicBoolean(false);
    private MediaRoute2ProviderServiceStub mStub;
    /** Populated by system_server in {@link #setCallback}. Monotonically non-null. */
    private IMediaRoute2ProviderServiceCallback mRemoteCallback;
    private volatile MediaRoute2ProviderInfo mProviderInfo;

    @GuardedBy("mRequestIdsLock")
    private final Deque<Long> mRequestIds = new ArrayDeque<>(MAX_REQUEST_IDS_SIZE);

    /**
     * Maps system media session creation request ids to a package uid whose media to route. The
     * value may be {@link Process#INVALID_UID} for routing sessions that don't affect a specific
     * package (for example, if they affect the entire system).
     */
    @GuardedBy("mRequestIdsLock")
    private final LongSparseArray<Integer> mSystemMediaSessionCreationRequests =
            new LongSparseArray<>();

    @GuardedBy("mSessionLock")
    private final ArrayMap<String, RoutingSessionInfo> mSessionInfos = new ArrayMap<>();

    @GuardedBy("mSessionLock")
    private final ArrayMap<String, MediaStreams> mOngoingMediaStreams = new ArrayMap<>();

    public MediaRoute2ProviderService() {
        mHandler = new Handler(Looper.getMainLooper());
    }
@@ -282,7 +312,7 @@ public abstract class MediaRoute2ProviderService extends Service {
     */
    public final void notifySessionCreated(long requestId,
            @NonNull RoutingSessionInfo sessionInfo) {
        Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
        requireNonNull(sessionInfo, "sessionInfo must not be null");

        if (DEBUG) {
            Log.d(TAG, "notifySessionCreated: Creating a session. requestId=" + requestId
@@ -326,17 +356,129 @@ public abstract class MediaRoute2ProviderService extends Service {
     * @param formats the {@link MediaStreamsFormats} that describes the format for the {@link
     *     MediaStreams} to return.
     * @return a {@link MediaStreams} instance that holds the media streams to route as part of the
     *     newly created routing session.
     *     newly created routing session. May be null if system media capture failed, in which case
     *     you can ignore the return value, as you will receive a call to {@link #onReleaseSession}
     *     where you can clean up this session
     * @hide
     */
    // TODO: b/362507305 - Unhide once the implementation and CTS are in place.
    @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
    @NonNull
    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
    @Nullable
    public final MediaStreams notifySystemMediaSessionCreated(
            long requestId,
            @NonNull RoutingSessionInfo sessionInfo,
            @NonNull MediaStreamsFormats formats) {
        throw new UnsupportedOperationException();
        requireNonNull(sessionInfo, "sessionInfo must not be null");
        requireNonNull(formats, "formats must not be null");
        if (DEBUG) {
            Log.d(
                    TAG,
                    "notifySystemMediaSessionCreated: Creating a session. requestId="
                            + requestId
                            + ", sessionInfo="
                            + sessionInfo);
        }

        Integer uid;
        synchronized (mRequestIdsLock) {
            uid = mSystemMediaSessionCreationRequests.get(requestId);
            mSystemMediaSessionCreationRequests.remove(requestId);
        }

        if (uid == null) {
            throw new IllegalStateException(
                    "Unexpected system routing session created (request id="
                            + requestId
                            + "):"
                            + sessionInfo);
        }

        if (mRemoteCallback == null) {
            throw new IllegalStateException("Unexpected: remote callback is null.");
        }

        int routingTypes = 0;
        var providerInfo = mProviderInfo;
        for (String selectedRouteId : sessionInfo.getSelectedRoutes()) {
            MediaRoute2Info route = providerInfo.mRoutes.get(selectedRouteId);
            if (route == null) {
                throw new IllegalArgumentException(
                        "Invalid selected route with id: " + selectedRouteId);
            }
            routingTypes |= route.getSupportedRoutingTypes();
        }

        if ((routingTypes & MediaRoute2Info.FLAG_ROUTING_TYPE_SYSTEM_AUDIO) == 0) {
            // TODO: b/380431086 - Populate video stream once we add support for video.
            throw new IllegalArgumentException(
                    "Selected routes for system media don't support any system media routing"
                            + " types.");
        }

        AudioFormat audioFormat = formats.mAudioFormat;
        var mediaStreamsBuilder = new MediaStreams.Builder();
        if (audioFormat != null) {
            populateAudioStream(audioFormat, uid, mediaStreamsBuilder);
        }
        // TODO: b/380431086 - Populate video stream once we add support for video.

        MediaStreams streams = mediaStreamsBuilder.build();
        var audioRecord = streams.mAudioRecord;
        if (audioRecord == null) {
            Log.e(
                    TAG,
                    "Audio record is not populated. Returning an empty stream and scheduling the"
                            + " session release for: "
                            + sessionInfo);
            mHandler.post(() -> onReleaseSession(REQUEST_ID_NONE, sessionInfo.getOriginalId()));
            notifyRequestFailed(requestId, REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA);
            return null;
        }

        synchronized (mSessionLock) {
            try {
                mRemoteCallback.notifySessionCreated(requestId, sessionInfo);
            } catch (RemoteException ex) {
                ex.rethrowFromSystemServer();
            }
            mOngoingMediaStreams.put(sessionInfo.getOriginalId(), streams);
            return streams;
        }
    }

    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
    private void populateAudioStream(
            AudioFormat audioFormat, int uid, MediaStreams.Builder builder) {
        var audioAttributes =
                new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
        var audioMixingRuleBuilder =
                new AudioMixingRule.Builder()
                        .addRule(audioAttributes, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
        if (uid != Process.INVALID_UID) {
            audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid);
        }

        AudioMix mix =
                new AudioMix.Builder(audioMixingRuleBuilder.build())
                        .setFormat(audioFormat)
                        .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
                        .build();
        AudioPolicy audioPolicy =
                new AudioPolicy.Builder(this).setLooper(mHandler.getLooper()).addMix(mix).build();
        var audioManager = getSystemService(AudioManager.class);
        if (audioManager == null) {
            Log.e(TAG, "Couldn't fetch the audio manager.");
            return;
        }
        audioManager.registerAudioPolicy(audioPolicy);
        var audioRecord = audioPolicy.createAudioRecordSink(mix);
        if (audioRecord == null) {
            Log.e(TAG, "Audio record creation failed.");
            audioManager.unregisterAudioPolicy(audioPolicy);
            return;
        }
        builder.setAudioStream(audioPolicy, audioRecord);
    }

    /**
@@ -344,7 +486,7 @@ public abstract class MediaRoute2ProviderService extends Service {
     * {@link RoutingSessionInfo#getSelectedRoutes() selected routes} are changed.
     */
    public final void notifySessionUpdated(@NonNull RoutingSessionInfo sessionInfo) {
        Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
        requireNonNull(sessionInfo, "sessionInfo must not be null");

        if (DEBUG) {
            Log.d(TAG, "notifySessionUpdated: Updating session id=" + sessionInfo);
@@ -379,6 +521,7 @@ public abstract class MediaRoute2ProviderService extends Service {
        RoutingSessionInfo sessionInfo;
        synchronized (mSessionLock) {
            sessionInfo = mSessionInfos.remove(sessionId);
            maybeReleaseMediaStreams(sessionId);

            if (sessionInfo == null) {
                Log.w(TAG, "notifySessionReleased: Ignoring unknown session info.");
@@ -396,6 +539,34 @@ public abstract class MediaRoute2ProviderService extends Service {
        }
    }

    /** Releases any system media routing resources associated with the given {@code sessionId}. */
    private void maybeReleaseMediaStreams(String sessionId) {
        if (!Flags.enableMirroringInMediaRouter2()) {
            return;
        }
        synchronized (mSessionLock) {
            var streams = mOngoingMediaStreams.remove(sessionId);
            if (streams != null) {
                releaseAudioStream(streams.mAudioPolicy, streams.mAudioRecord);
                // TODO: b/380431086: Release the video stream once implemented.
            }
        }
    }

    // We cannot reach the code that requires MODIFY_AUDIO_ROUTING without holding it.
    @SuppressWarnings("MissingPermission")
    private void releaseAudioStream(AudioPolicy audioPolicy, AudioRecord audioRecord) {
        if (audioPolicy == null) {
            return;
        }
        var audioManager = getSystemService(AudioManager.class);
        if (audioManager == null) {
            return;
        }
        audioRecord.stop();
        audioManager.unregisterAudioPolicy(audioPolicy);
    }

    /**
     * Notifies to the client that the request has failed.
     *
@@ -569,7 +740,7 @@ public abstract class MediaRoute2ProviderService extends Service {
     * Updates routes of the provider and notifies the system media router service.
     */
    public final void notifyRoutes(@NonNull Collection<MediaRoute2Info> routes) {
        Objects.requireNonNull(routes, "routes must not be null");
        requireNonNull(routes, "routes must not be null");
        List<MediaRoute2Info> sanitizedRoutes = new ArrayList<>(routes.size());

        for (MediaRoute2Info route : routes) {
@@ -762,6 +933,32 @@ public abstract class MediaRoute2ProviderService extends Service {
                    requestCreateSession));
        }

        @Override
        public void requestCreateSystemMediaSession(
                long requestId,
                int uid,
                String packageName,
                String routeId,
                @Nullable Bundle sessionHints) {
            if (!checkCallerIsSystem()) {
                return;
            }
            if (!checkRouteIdIsValid(routeId, "requestCreateSession")) {
                return;
            }
            synchronized (mRequestIdsLock) {
                mSystemMediaSessionCreationRequests.put(requestId, uid);
            }
            mHandler.sendMessage(
                    obtainMessage(
                            MediaRoute2ProviderService::onCreateSystemRoutingSession,
                            MediaRoute2ProviderService.this,
                            requestId,
                            packageName,
                            routeId,
                            sessionHints));
        }

        @Override
        public void selectRoute(long requestId, String sessionId, String routeId) {
            if (!checkCallerIsSystem()) {
@@ -825,6 +1022,10 @@ public abstract class MediaRoute2ProviderService extends Service {
            if (!checkSessionIdIsValid(sessionId, "releaseSession")) {
                return;
            }
            // We proactively release the system media routing once the system requests it, to
            // ensure it happens immediately.
            maybeReleaseMediaStreams(sessionId);

            addRequestId(requestId);
            mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onReleaseSession,
                    MediaRoute2ProviderService.this, requestId, sessionId));
@@ -843,12 +1044,14 @@ public abstract class MediaRoute2ProviderService extends Service {
    @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
    public static final class MediaStreams {

        private final AudioRecord mAudioRecord;
        @Nullable private final AudioPolicy mAudioPolicy;
        @Nullable private final AudioRecord mAudioRecord;

        // TODO: b/380431086: Add the video equivalent.

        private MediaStreams(AudioRecord mAudioRecord) {
            this.mAudioRecord = mAudioRecord;
        private MediaStreams(Builder builder) {
            this.mAudioPolicy = builder.mAudioPolicy;
            this.mAudioRecord = builder.mAudioRecord;
        }

        /**
@@ -859,7 +1062,32 @@ public abstract class MediaRoute2ProviderService extends Service {
        public AudioRecord getAudioRecord() {
            return mAudioRecord;
        }

        /**
         * Builder for {@link MediaStreams}.
         *
         * @hide
         */
        public static final class Builder {

            @Nullable private AudioPolicy mAudioPolicy;
            @Nullable private AudioRecord mAudioRecord;

            /** Populates system media audio-related structures. */
            public Builder setAudioStream(
                    @NonNull AudioPolicy audioPolicy, @NonNull AudioRecord audioRecord) {
                mAudioPolicy = requireNonNull(audioPolicy);
                mAudioRecord = requireNonNull(audioRecord);
                return this;
            }

            /** Builds a {@link MediaStreams} instance. */
            public MediaStreams build() {
                return new MediaStreams(this);
            }
        }
    }


    /**
     * Holds the formats to encode media data to be read from {@link MediaStreams}.
@@ -911,7 +1139,7 @@ public abstract class MediaRoute2ProviderService extends Service {
             */
            @NonNull
            public Builder setAudioFormat(@NonNull AudioFormat audioFormat) {
                this.mAudioFormat = Objects.requireNonNull(audioFormat);
                this.mAudioFormat = requireNonNull(audioFormat);
                return this;
            }