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

Commit 267ae6fd authored by Iván Budnik's avatar Iván Budnik
Browse files

Revamp MediaRouter2 scanning API and add support for screen off scanning

This change adds support for companion-app-related routing use cases and
addresses limitations with the previous scanning mechanisms. The design
and motivation is described in go/revamp-mr2-scanning-api.

Test: atest CtsMediaBetterTogetherTestCases
Bug: 281072508
Change-Id: I3045c7213a29f7f35b578391cfe19b49aa9748dc
Flag: ACONFIG com.android.media.flags.enable_screen_off_scanning DEVELOPMENT
parent 839e64c6
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -24376,6 +24376,7 @@ package android.media {
  }
  public final class MediaRouter2 {
    method @FlaggedApi("com.android.media.flags.enable_screen_off_scanning") public void cancelScanRequest(@NonNull android.media.MediaRouter2.ScanToken);
    method @Nullable public android.media.MediaRouter2.RoutingController getController(@NonNull String);
    method @NonNull public java.util.List<android.media.MediaRouter2.RoutingController> getControllers();
    method @NonNull public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context);
@@ -24387,6 +24388,7 @@ package android.media {
    method public void registerRouteCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaRouter2.RouteCallback, @NonNull android.media.RouteDiscoveryPreference);
    method @FlaggedApi("com.android.media.flags.enable_rlp_callbacks_in_media_router2") public void registerRouteListingPreferenceUpdatedCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.media.RouteListingPreference>);
    method public void registerTransferCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaRouter2.TransferCallback);
    method @FlaggedApi("com.android.media.flags.enable_screen_off_scanning") @NonNull public android.media.MediaRouter2.ScanToken requestScan(@NonNull android.media.MediaRouter2.ScanRequest);
    method public void setOnGetControllerHintsListener(@Nullable android.media.MediaRouter2.OnGetControllerHintsListener);
    method public void setRouteListingPreference(@Nullable android.media.RouteListingPreference);
    method public boolean showSystemOutputSwitcher();
@@ -24433,6 +24435,19 @@ package android.media {
    method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public boolean wasTransferInitiatedBySelf();
  }
  @FlaggedApi("com.android.media.flags.enable_screen_off_scanning") public static final class MediaRouter2.ScanRequest {
    method public boolean isScreenOffScan();
  }
  public static final class MediaRouter2.ScanRequest.Builder {
    ctor public MediaRouter2.ScanRequest.Builder();
    method @NonNull public android.media.MediaRouter2.ScanRequest build();
    method @NonNull public android.media.MediaRouter2.ScanRequest.Builder setScreenOffScan(boolean);
  }
  @FlaggedApi("com.android.media.flags.enable_screen_off_scanning") public static final class MediaRouter2.ScanToken {
  }
  public abstract static class MediaRouter2.TransferCallback {
    ctor public MediaRouter2.TransferCallback();
    method public void onStop(@NonNull android.media.MediaRouter2.RoutingController);
+2 −3
Original line number Diff line number Diff line
@@ -27,7 +27,6 @@ import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
import android.os.Bundle;
import android.os.UserHandle;

/**
 * {@hide}
 */
@@ -56,6 +55,7 @@ interface IMediaRouterService {

    void registerRouter2(IMediaRouter2 router, String packageName);
    void unregisterRouter2(IMediaRouter2 router);
    void updateScanningStateWithRouter2(IMediaRouter2 router, @JavaPassthrough(annotation="@android.media.MediaRouter2.ScanningState") int scanningState);
    void setDiscoveryRequestWithRouter2(IMediaRouter2 router,
            in RouteDiscoveryPreference preference);
    void setRouteListingPreference(IMediaRouter2 router,
@@ -81,8 +81,7 @@ interface IMediaRouterService {
    void unregisterManager(IMediaRouter2Manager manager);
    void setRouteVolumeWithManager(IMediaRouter2Manager manager, int requestId,
            in MediaRoute2Info route, int volume);
    void startScan(IMediaRouter2Manager manager);
    void stopScan(IMediaRouter2Manager manager);
    void updateScanningState(IMediaRouter2Manager manager, @JavaPassthrough(annotation="@android.media.MediaRouter2.ScanningState") int scanningState);

    void requestCreateSessionWithManager(IMediaRouter2Manager manager, int requestId,
            in RoutingSessionInfo oldSession, in @nullable MediaRoute2Info route,
+250 −6
Original line number Diff line number Diff line
@@ -20,10 +20,12 @@ import static com.android.internal.util.function.pooled.PooledLambda.obtainMessa
import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES;
import static com.android.media.flags.Flags.FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2;
import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2;
import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING;

import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -42,9 +44,12 @@ import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;

import com.android.internal.annotations.GuardedBy;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -73,6 +78,48 @@ import java.util.stream.Collectors;
//       Not only MediaRouter2, but also to service / manager / provider.
// TODO: ensure thread-safe and document it
public final class MediaRouter2 {

    /**
     * The state of a router not requesting route scanning.
     *
     * @hide
     */
    public static final int SCANNING_STATE_NOT_SCANNING = 0;

    /**
     * The state of a router requesting scanning only while the user interacts with its owner app.
     *
     * <p>The device's screen must be on and the app must be in the foreground to trigger scanning
     * under this state.
     *
     * @hide
     */
    public static final int SCANNING_STATE_WHILE_INTERACTIVE = 1;

    /**
     * The state of a router requesting unrestricted scanning.
     *
     * <p>This state triggers scanning regardless of the restrictions required for {@link
     * #SCANNING_STATE_WHILE_INTERACTIVE}.
     *
     * <p>Routers requesting unrestricted scanning must hold {@link
     * Manifest.permission#MEDIA_ROUTING_CONTROL}.
     *
     * @hide
     */
    public static final int SCANNING_STATE_SCANNING_FULL = 2;

    /** @hide */
    @IntDef(
            prefix = "SCANNING_STATE",
            value = {
                SCANNING_STATE_NOT_SCANNING,
                SCANNING_STATE_WHILE_INTERACTIVE,
                SCANNING_STATE_SCANNING_FULL
            })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ScanningState {}

    private static final String TAG = "MR2";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    private static final Object sSystemRouterLock = new Object();
@@ -123,6 +170,13 @@ public final class MediaRouter2 {
    @GuardedBy("mLock")
    private final Map<String, RoutingController> mNonSystemRoutingControllers = new ArrayMap<>();

    @GuardedBy("mLock")
    private int mScreenOffScanRequestCount = 0;

    @GuardedBy("mLock")
    private int mScreenOnScanRequestCount = 0;

    private final SparseArray<ScanRequest> mScanRequestsMap = new SparseArray<>();
    private final AtomicInteger mNextRequestId = new AtomicInteger(1);
    private final Handler mHandler;

@@ -335,6 +389,100 @@ public final class MediaRouter2 {
        mImpl.stopScan();
    }

    /**
     * Requests the system to actively scan for routes based on the router's {@link
     * RouteDiscoveryPreference route discovery preference}.
     *
     * <p>You must call {@link #cancelScanRequest(ScanToken)} promptly to preserve system resources
     * like battery. Avoid scanning unless there is clear intention from the user to start routing
     * their media.
     *
     * <p>{@code scanRequest} specifies relevant scanning options, like whether the system should
     * scan with the screen off. Screen off scanning requires {@link
     * Manifest.permission#MEDIA_ROUTING_CONTROL}
     *
     * <p>Proxy routers use the registered {@link RouteDiscoveryPreference} of their target routers.
     *
     * @return A unique {@link ScanToken} that identifies the scan request.
     */
    @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
    @NonNull
    public ScanToken requestScan(@NonNull ScanRequest scanRequest) {
        Objects.requireNonNull(scanRequest, "scanRequest must not be null.");
        ScanToken token = new ScanToken(mNextRequestId.getAndIncrement());

        synchronized (mLock) {
            boolean shouldUpdate =
                    mScreenOffScanRequestCount == 0
                            && (scanRequest.isScreenOffScan() || mScreenOnScanRequestCount == 0);

            if (shouldUpdate) {
                try {
                    mImpl.updateScanningState(
                            scanRequest.isScreenOffScan()
                                    ? SCANNING_STATE_SCANNING_FULL
                                    : SCANNING_STATE_WHILE_INTERACTIVE);

                    if (scanRequest.isScreenOffScan()) {
                        mScreenOffScanRequestCount++;
                    } else {
                        mScreenOnScanRequestCount++;
                    }
                } catch (RemoteException ex) {
                    throw ex.rethrowFromSystemServer();
                }
            }

            mScanRequestsMap.put(token.mId, scanRequest);
            return token;
        }
    }

    /**
     * Releases the active scan request linked to the provided {@link ScanToken}.
     *
     * @see #requestScan(ScanRequest)
     * @param token {@link ScanToken} of the {@link ScanRequest} to release.
     * @throws IllegalArgumentException if the token does not match any active scan request.
     */
    @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
    public void cancelScanRequest(@NonNull ScanToken token) {
        Objects.requireNonNull(token, "token must not be null");

        synchronized (mLock) {
            ScanRequest request = mScanRequestsMap.get(token.mId);

            if (request == null) {
                throw new IllegalArgumentException(
                        "The token does not match any active scan request");
            }

            boolean shouldUpdate =
                    mScreenOffScanRequestCount == 1
                            && (request.isScreenOffScan() || mScreenOnScanRequestCount == 1);

            if (shouldUpdate) {
                try {
                    if (request.isScreenOffScan() && mScreenOnScanRequestCount == 0) {
                        mImpl.updateScanningState(SCANNING_STATE_NOT_SCANNING);
                    } else {
                        mImpl.updateScanningState(SCANNING_STATE_WHILE_INTERACTIVE);
                    }

                    if (request.isScreenOffScan()) {
                        mScreenOffScanRequestCount--;
                    } else {
                        mScreenOnScanRequestCount--;
                    }
                } catch (RemoteException ex) {
                    ex.rethrowFromSystemServer();
                }
            }

            mScanRequestsMap.remove(token.mId);
        }
    }

    private MediaRouter2(Context appContext) {
        mContext = appContext;
        mMediaRouterService =
@@ -1428,6 +1576,78 @@ public final class MediaRouter2 {
        public void onControllerUpdated(@NonNull RoutingController controller) {}
    }

    /**
     * Represents an active scan request registered in the system.
     *
     * <p>See {@link #requestScan(ScanRequest)} for more information.
     */
    @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
    public static final class ScanToken {
        private final int mId;

        private ScanToken(int id) {
            mId = id;
        }
    }

    /**
     * Represents a set of parameters for scanning requests.
     *
     * <p>See {@link #requestScan(ScanRequest)} for more details.
     */
    @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
    public static final class ScanRequest {
        private final boolean mIsScreenOffScan;

        private ScanRequest(boolean isScreenOffScan) {
            mIsScreenOffScan = isScreenOffScan;
        }

        /**
         * Returns whether the scan request corresponds to a screen-off scan.
         *
         * @see #requestScan(ScanRequest)
         */
        public boolean isScreenOffScan() {
            return mIsScreenOffScan;
        }

        /**
         * Builder class for {@link ScanRequest}.
         *
         * @see #requestScan(ScanRequest)
         */
        public static final class Builder {
            boolean mIsScreenOffScan;

            /**
             * Creates a builder for a {@link ScanRequest} instance.
             *
             * @see #requestScan(ScanRequest)
             */
            public Builder() {}

            /**
             * Sets whether the app is requesting to scan even while the screen is off, bypassing
             * default scanning restrictions. Only companion apps holding {@link
             * Manifest.permission#MEDIA_ROUTING_CONTROL} should set this to {@code true}.
             *
             * @see #requestScan(ScanRequest)
             */
            @NonNull
            public Builder setScreenOffScan(boolean isScreenOffScan) {
                mIsScreenOffScan = isScreenOffScan;
                return this;
            }

            /** Returns a new {@link ScanRequest} instance. */
            @NonNull
            public ScanRequest build() {
                return new ScanRequest(mIsScreenOffScan);
            }
        }
    }

    /**
     * A class to control media routing session in media route provider. For example,
     * selecting/deselecting/transferring to routes of a session can be done through this. Instances
@@ -2092,6 +2312,9 @@ public final class MediaRouter2 {
     * ProxyMediaRouter2Impl proxy} {@link MediaRouter2} instances.
     */
    private interface MediaRouter2Impl {

        void updateScanningState(@ScanningState int scanningState) throws RemoteException;

        void startScan();

        void stopScan();
@@ -2194,12 +2417,18 @@ public final class MediaRouter2 {
            }
        }

        @Override
        public void updateScanningState(int scanningState) throws RemoteException {
            mMediaRouterService.updateScanningState(mClient, scanningState);
        }

        @Override
        public void startScan() {
            if (!mIsScanning.getAndSet(true)) {
                if (mScanRequestCount.getAndIncrement() == 0) {
                    try {
                        mMediaRouterService.startScan(mClient);
                        mMediaRouterService.updateScanningState(
                                mClient, SCANNING_STATE_WHILE_INTERACTIVE);
                    } catch (RemoteException ex) {
                        throw ex.rethrowFromSystemServer();
                    }
@@ -2221,7 +2450,8 @@ public final class MediaRouter2 {
                                })
                        == 0) {
                    try {
                        mMediaRouterService.stopScan(mClient);
                        mMediaRouterService.updateScanningState(
                                mClient, SCANNING_STATE_NOT_SCANNING);
                    } catch (RemoteException ex) {
                        throw ex.rethrowFromSystemServer();
                    }
@@ -3041,6 +3271,18 @@ public final class MediaRouter2 {
            // Do nothing.
        }

        @Override
        @GuardedBy("mLock")
        public void updateScanningState(int scanningState) throws RemoteException {
            if (scanningState != SCANNING_STATE_NOT_SCANNING) {
                registerRouterStubIfNeededLocked();
            }
            mMediaRouterService.updateScanningStateWithRouter2(mStub, scanningState);
            if (scanningState == SCANNING_STATE_NOT_SCANNING) {
                unregisterRouterStubIfNeededLocked(/* isScanningStopping */ true);
            }
        }

        /**
         * Returns {@code null}. The client package name is only associated to proxy {@link
         * MediaRouter2} instances.
@@ -3103,7 +3345,7 @@ public final class MediaRouter2 {
                                mStub, mDiscoveryPreference);
                    }

                    unregisterRouterStubIfNeededLocked();
                    unregisterRouterStubIfNeededLocked(/* isScanningStopping */ false);

                } catch (RemoteException ex) {
                    Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex);
@@ -3313,7 +3555,7 @@ public final class MediaRouter2 {
                }

                try {
                    unregisterRouterStubIfNeededLocked();
                    unregisterRouterStubIfNeededLocked(/* isScanningStopping */ false);
                } catch (RemoteException ex) {
                    ex.rethrowFromSystemServer();
                }
@@ -3331,10 +3573,12 @@ public final class MediaRouter2 {
        }

        @GuardedBy("mLock")
        private void unregisterRouterStubIfNeededLocked() throws RemoteException {
        private void unregisterRouterStubIfNeededLocked(boolean isScanningStopping)
                throws RemoteException {
            if (mStub != null
                    && mRouteCallbackRecords.isEmpty()
                    && mNonSystemRoutingControllers.isEmpty()) {
                    && mNonSystemRoutingControllers.isEmpty()
                    && (mScanRequestsMap.size() == 0 || isScanningStopping)) {
                mMediaRouterService.unregisterRouter2(mStub);
                mStub = null;
            }
+5 −2
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

package android.media;

import static android.media.MediaRouter2.SCANNING_STATE_NOT_SCANNING;
import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE;

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

import android.Manifest;
@@ -174,7 +177,7 @@ public final class MediaRouter2Manager {
    public void registerScanRequest() {
        if (mScanRequestCount.getAndIncrement() == 0) {
            try {
                mMediaRouterService.startScan(mClient);
                mMediaRouterService.updateScanningState(mClient, SCANNING_STATE_WHILE_INTERACTIVE);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
@@ -201,7 +204,7 @@ public final class MediaRouter2Manager {
                })
                == 0) {
            try {
                mMediaRouterService.stopScan(mClient);
                mMediaRouterService.updateScanningState(mClient, SCANNING_STATE_NOT_SCANNING);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
+7 −0
Original line number Diff line number Diff line
@@ -90,3 +90,10 @@ flag {
    description: "Enables mechanisms to prevent route providers from keeping malicious apps alive."
    bug: "263520343"
}

flag {
    name: "enable_screen_off_scanning"
    namespace: "media_solutions"
    description: "Enable new MediaRouter2 API to enable watch companion apps to scan while the phone screen is off."
    bug: "281072508"
}
Loading