diff options
| author | 2024-01-19 15:42:05 +0000 | |
|---|---|---|
| committer | 2024-02-08 15:53:43 +0000 | |
| commit | 267ae6fde172e259e53364e1996ae2b9233f0c2b (patch) | |
| tree | 3a359a758b7a0f0c02de86cf27baeebc9b346aa6 | |
| parent | 839e64c6472881e16a39de4ffa18bc3aa245971a (diff) | |
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
7 files changed, 511 insertions, 107 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index d410686c9ea1..7be8319e86ae 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -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); diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 04e99ea8a57f..7727078a42ec 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -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, diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 425db06ce55f..062c44275223 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -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 = @@ -1429,6 +1577,78 @@ public final class MediaRouter2 { } /** + * 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 * are created when {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is @@ -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(); @@ -2195,11 +2418,17 @@ 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; } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 06c0996785c2..488d54486561 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -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(); } diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index df9ecdc98e85..9e812f349d79 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -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" +} diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 85a131579497..1f7d5490dd6d 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -20,6 +20,9 @@ import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREG import static android.content.Intent.ACTION_SCREEN_OFF; import static android.content.Intent.ACTION_SCREEN_ON; import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR; +import static android.media.MediaRouter2.SCANNING_STATE_NOT_SCANNING; +import static android.media.MediaRouter2.SCANNING_STATE_SCANNING_FULL; +import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE; import static android.media.MediaRouter2Utils.getOriginalId; import static android.media.MediaRouter2Utils.getProviderId; @@ -42,6 +45,7 @@ import android.media.IMediaRouter2Manager; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; import android.media.MediaRoute2ProviderService; +import android.media.MediaRouter2.ScanningState; import android.media.MediaRouter2Manager; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; @@ -224,17 +228,27 @@ class MediaRouter2ServiceImpl { final int uid = Binder.getCallingUid(); final int pid = Binder.getCallingPid(); final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); - final boolean hasConfigureWifiDisplayPermission = mContext.checkCallingOrSelfPermission( - android.Manifest.permission.CONFIGURE_WIFI_DISPLAY) - == PackageManager.PERMISSION_GRANTED; + final boolean hasConfigureWifiDisplayPermission = + mContext.checkCallingOrSelfPermission(Manifest.permission.CONFIGURE_WIFI_DISPLAY) + == PackageManager.PERMISSION_GRANTED; final boolean hasModifyAudioRoutingPermission = checkCallerHasModifyAudioRoutingPermission(pid, uid); + boolean hasMediaRoutingControlPermission = + checkMediaRoutingControlPermission(uid, pid, packageName); + final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - registerRouter2Locked(router, uid, pid, packageName, userId, - hasConfigureWifiDisplayPermission, hasModifyAudioRoutingPermission); + registerRouter2Locked( + router, + uid, + pid, + packageName, + userId, + hasConfigureWifiDisplayPermission, + hasModifyAudioRoutingPermission, + hasMediaRoutingControlPermission); } } finally { Binder.restoreCallingIdentity(token); @@ -254,6 +268,21 @@ class MediaRouter2ServiceImpl { } } + public void updateScanningState( + @NonNull IMediaRouter2 router, @ScanningState int scanningState) { + Objects.requireNonNull(router, "router must not be null"); + validateScanningStateValue(scanningState); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + updateScanningStateLocked(router, scanningState); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + public void setDiscoveryRequestWithRouter2(@NonNull IMediaRouter2 router, @NonNull RouteDiscoveryPreference preference) { Objects.requireNonNull(router, "router must not be null"); @@ -570,24 +599,15 @@ class MediaRouter2ServiceImpl { } } - public void startScan(@NonNull IMediaRouter2Manager manager) { + public void updateScanningState( + @NonNull IMediaRouter2Manager manager, @ScanningState int scanningState) { Objects.requireNonNull(manager, "manager must not be null"); - final long token = Binder.clearCallingIdentity(); - try { - synchronized (mLock) { - startScanLocked(manager); - } - } finally { - Binder.restoreCallingIdentity(token); - } - } + validateScanningStateValue(scanningState); - public void stopScan(@NonNull IMediaRouter2Manager manager) { - Objects.requireNonNull(manager, "manager must not be null"); final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - stopScanLocked(manager); + updateScanningStateLocked(manager, scanningState); } } finally { Binder.restoreCallingIdentity(token); @@ -825,7 +845,16 @@ class MediaRouter2ServiceImpl { throw new SecurityException("Must hold MEDIA_CONTENT_CONTROL"); } - if (PermissionChecker.checkPermissionForDataDelivery( + if (!checkMediaRoutingControlPermission(callerUid, callerPid, callerPackageName)) { + throw new SecurityException( + "Must hold MEDIA_CONTENT_CONTROL or MEDIA_ROUTING_CONTROL permissions."); + } + } + + @RequiresPermission(value = Manifest.permission.MEDIA_ROUTING_CONTROL, conditional = true) + private boolean checkMediaRoutingControlPermission( + int callerUid, int callerPid, @Nullable String callerPackageName) { + return PermissionChecker.checkPermissionForDataDelivery( mContext, Manifest.permission.MEDIA_ROUTING_CONTROL, callerPid, @@ -833,11 +862,8 @@ class MediaRouter2ServiceImpl { callerPackageName, /* attributionTag */ null, /* message */ "Checking permissions for registering manager in" - + " MediaRouter2ServiceImpl.") - != PermissionChecker.PERMISSION_GRANTED) { - throw new SecurityException( - "Must hold MEDIA_CONTENT_CONTROL or MEDIA_ROUTING_CONTROL permissions."); - } + + " MediaRouter2ServiceImpl.") + == PermissionChecker.PERMISSION_GRANTED; } @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS) @@ -944,9 +970,15 @@ class MediaRouter2ServiceImpl { // Start of locked methods that are used by MediaRouter2. @GuardedBy("mLock") - private void registerRouter2Locked(@NonNull IMediaRouter2 router, int uid, int pid, - @NonNull String packageName, int userId, boolean hasConfigureWifiDisplayPermission, - boolean hasModifyAudioRoutingPermission) { + private void registerRouter2Locked( + @NonNull IMediaRouter2 router, + int uid, + int pid, + @NonNull String packageName, + int userId, + boolean hasConfigureWifiDisplayPermission, + boolean hasModifyAudioRoutingPermission, + boolean hasMediaRoutingControlPermission) { final IBinder binder = router.asBinder(); if (mAllRouterRecords.get(binder) != null) { Slog.w(TAG, "registerRouter2Locked: Same router already exists. packageName=" @@ -955,8 +987,16 @@ class MediaRouter2ServiceImpl { } UserRecord userRecord = getOrCreateUserRecordLocked(userId); - RouterRecord routerRecord = new RouterRecord(userRecord, router, uid, pid, packageName, - hasConfigureWifiDisplayPermission, hasModifyAudioRoutingPermission); + RouterRecord routerRecord = + new RouterRecord( + userRecord, + router, + uid, + pid, + packageName, + hasConfigureWifiDisplayPermission, + hasModifyAudioRoutingPermission, + hasMediaRoutingControlPermission); try { binder.linkToDeath(routerRecord, 0); } catch (RemoteException ex) { @@ -970,9 +1010,16 @@ class MediaRouter2ServiceImpl { obtainMessage(UserHandler::notifyRouterRegistered, userRecord.mHandler, routerRecord)); - Slog.i(TAG, TextUtils.formatSimple( - "registerRouter2 | package: %s, uid: %d, pid: %d, router id: %d", - packageName, uid, pid, routerRecord.mRouterId)); + Slog.i( + TAG, + TextUtils.formatSimple( + "registerRouter2 | package: %s, uid: %d, pid: %d, router id: %d," + + " hasMediaRoutingControl: %b", + packageName, + uid, + pid, + routerRecord.mRouterId, + hasMediaRoutingControlPermission)); } @GuardedBy("mLock") @@ -1012,6 +1059,33 @@ class MediaRouter2ServiceImpl { } @GuardedBy("mLock") + private void updateScanningStateLocked( + @NonNull IMediaRouter2 router, @ScanningState int scanningState) { + final IBinder binder = router.asBinder(); + RouterRecord routerRecord = mAllRouterRecords.get(binder); + if (routerRecord == null) { + Slog.w(TAG, "Router record not found. Ignoring updateScanningState call."); + return; + } + + if (scanningState == SCANNING_STATE_SCANNING_FULL + && !routerRecord.mHasMediaRoutingControl) { + throw new SecurityException("Screen off scan requires MEDIA_ROUTING_CONTROL"); + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "updateScanningStateLocked | router: %d, packageName: %s, scanningState:" + + " %d", + routerRecord.mRouterId, + routerRecord.mPackageName, + getScanningStateString(scanningState))); + + routerRecord.updateScanningState(scanningState); + } + + @GuardedBy("mLock") private void setDiscoveryRequestWithRouter2Locked(@NonNull RouterRecord routerRecord, @NonNull RouteDiscoveryPreference discoveryRequest) { if (routerRecord.mDiscoveryPreference.equals(discoveryRequest)) { @@ -1347,14 +1421,24 @@ class MediaRouter2ServiceImpl { return; } + boolean hasMediaRoutingControl = + checkMediaRoutingControlPermission(callerUid, callerPid, callerPackageName); + Slog.i( TAG, TextUtils.formatSimple( "registerManager | callerUid: %d, callerPid: %d, callerPackage: %s," - + "targetPackageName: %s, targetUserId: %d", - callerUid, callerPid, callerPackageName, targetPackageName, targetUser)); + + " targetPackageName: %s, targetUserId: %d, hasMediaRoutingControl:" + + " %b", + callerUid, + callerPid, + callerPackageName, + targetPackageName, + targetUser, + hasMediaRoutingControl)); UserRecord userRecord = getOrCreateUserRecordLocked(targetUser.getIdentifier()); + managerRecord = new ManagerRecord( userRecord, @@ -1362,7 +1446,8 @@ class MediaRouter2ServiceImpl { callerUid, callerPid, callerPackageName, - targetPackageName); + targetPackageName, + hasMediaRoutingControl); try { binder.linkToDeath(managerRecord, 0); } catch (RemoteException ex) { @@ -1427,41 +1512,31 @@ class MediaRouter2ServiceImpl { } @GuardedBy("mLock") - private void startScanLocked(@NonNull IMediaRouter2Manager manager) { + private void updateScanningStateLocked( + @NonNull IMediaRouter2Manager manager, @ScanningState int scanningState) { final IBinder binder = manager.asBinder(); ManagerRecord managerRecord = mAllManagerRecords.get(binder); if (managerRecord == null) { + Slog.w(TAG, "Manager record not found. Ignoring updateScanningState call."); return; } - Slog.i( - TAG, - TextUtils.formatSimple( - "startScan | manager: %d, ownerPackageName: %s, targetPackageName: %s", - managerRecord.mManagerId, - managerRecord.mOwnerPackageName, - managerRecord.mTargetPackageName)); - - managerRecord.startScan(); - } - - @GuardedBy("mLock") - private void stopScanLocked(@NonNull IMediaRouter2Manager manager) { - final IBinder binder = manager.asBinder(); - ManagerRecord managerRecord = mAllManagerRecords.get(binder); - if (managerRecord == null) { - return; + if (!managerRecord.mHasMediaRoutingControl + && scanningState == SCANNING_STATE_SCANNING_FULL) { + throw new SecurityException("Screen off scan requires MEDIA_ROUTING_CONTROL"); } Slog.i( TAG, TextUtils.formatSimple( - "stopScan | manager: %d, ownerPackageName: %s, targetPackageName: %s", + "updateScanningState | manager: %d, ownerPackageName: %s," + + " targetPackageName: %s, scanningState: %d", managerRecord.mManagerId, managerRecord.mOwnerPackageName, - managerRecord.mTargetPackageName)); + managerRecord.mTargetPackageName, + getScanningStateString(scanningState))); - managerRecord.stopScan(); + managerRecord.updateScanningState(scanningState); } @GuardedBy("mLock") @@ -1738,6 +1813,24 @@ class MediaRouter2ServiceImpl { return (int) uniqueRequestId; } + private static String getScanningStateString(@ScanningState int scanningState) { + return switch (scanningState) { + case SCANNING_STATE_NOT_SCANNING -> "NOT_SCANNING"; + case SCANNING_STATE_WHILE_INTERACTIVE -> "SCREEN_ON_ONLY"; + case SCANNING_STATE_SCANNING_FULL -> "FULL"; + default -> "Invalid scanning state: " + scanningState; + }; + } + + private static void validateScanningStateValue(@ScanningState int scanningState) { + if (scanningState != SCANNING_STATE_NOT_SCANNING + && scanningState != SCANNING_STATE_WHILE_INTERACTIVE + && scanningState != SCANNING_STATE_SCANNING_FULL) { + throw new IllegalArgumentException( + TextUtils.formatSimple("Scanning state %d is not valid.", scanningState)); + } + } + final class UserRecord { public final int mUserId; //TODO: make records private for thread-safety @@ -1817,13 +1910,21 @@ class MediaRouter2ServiceImpl { public final boolean mHasModifyAudioRoutingPermission; public final AtomicBoolean mHasBluetoothRoutingPermission; public final int mRouterId; + public final boolean mHasMediaRoutingControl; + public @ScanningState int mScanningState = SCANNING_STATE_NOT_SCANNING; public RouteDiscoveryPreference mDiscoveryPreference; @Nullable public RouteListingPreference mRouteListingPreference; - RouterRecord(UserRecord userRecord, IMediaRouter2 router, int uid, int pid, - String packageName, boolean hasConfigureWifiDisplayPermission, - boolean hasModifyAudioRoutingPermission) { + RouterRecord( + UserRecord userRecord, + IMediaRouter2 router, + int uid, + int pid, + String packageName, + boolean hasConfigureWifiDisplayPermission, + boolean hasModifyAudioRoutingPermission, + boolean hasMediaRoutingControl) { mUserRecord = userRecord; mPackageName = packageName; mSelectRouteSequenceNumbers = new ArrayList<>(); @@ -1835,6 +1936,7 @@ class MediaRouter2ServiceImpl { mHasModifyAudioRoutingPermission = hasModifyAudioRoutingPermission; mHasBluetoothRoutingPermission = new AtomicBoolean(checkCallerHasBluetoothPermissions(mPid, mUid)); + mHasMediaRoutingControl = hasMediaRoutingControl; mRouterId = mNextRouterOrManagerId.getAndIncrement(); } @@ -1846,6 +1948,12 @@ class MediaRouter2ServiceImpl { return mHasModifyAudioRoutingPermission || mHasBluetoothRoutingPermission.get(); } + public boolean isActivelyScanning() { + return mScanningState == SCANNING_STATE_WHILE_INTERACTIVE + || mScanningState == SCANNING_STATE_SCANNING_FULL + || mDiscoveryPreference.shouldPerformActiveScan(); + } + @GuardedBy("mLock") public void maybeUpdateSystemRoutingPermissionLocked() { boolean oldSystemRoutingPermissionValue = hasSystemRoutingPermission(); @@ -1877,6 +1985,18 @@ class MediaRouter2ServiceImpl { routerDied(this); } + public void updateScanningState(@ScanningState int scanningState) { + if (mScanningState == scanningState) { + return; + } + + mScanningState = scanningState; + + mUserRecord.mHandler.sendMessage( + obtainMessage( + UserHandler::updateDiscoveryPreferenceOnHandler, mUserRecord.mHandler)); + } + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { pw.println(prefix + "RouterRecord"); @@ -2002,8 +2122,11 @@ class MediaRouter2ServiceImpl { public final int mManagerId; // TODO (b/281072508): Document behaviour around nullability for mTargetPackageName. @Nullable public final String mTargetPackageName; + + public final boolean mHasMediaRoutingControl; @Nullable public SessionCreationRequest mLastSessionCreationRequest; - public boolean mIsScanning; + + public @ScanningState int mScanningState = SCANNING_STATE_NOT_SCANNING; ManagerRecord( @NonNull UserRecord userRecord, @@ -2011,7 +2134,8 @@ class MediaRouter2ServiceImpl { int ownerUid, int ownerPid, @NonNull String ownerPackageName, - @Nullable String targetPackageName) { + @Nullable String targetPackageName, + boolean hasMediaRoutingControl) { mUserRecord = userRecord; mManager = manager; mOwnerUid = ownerUid; @@ -2019,6 +2143,7 @@ class MediaRouter2ServiceImpl { mOwnerPackageName = ownerPackageName; mTargetPackageName = targetPackageName; mManagerId = mNextRouterOrManagerId.getAndIncrement(); + mHasMediaRoutingControl = hasMediaRoutingControl; } public void dispose() { @@ -2040,29 +2165,23 @@ class MediaRouter2ServiceImpl { pw.println(indent + "mManagerId=" + mManagerId); pw.println(indent + "mOwnerUid=" + mOwnerUid); pw.println(indent + "mOwnerPid=" + mOwnerPid); - pw.println(indent + "mIsScanning=" + mIsScanning); + pw.println(indent + "mScanningState=" + getScanningStateString(mScanningState)); if (mLastSessionCreationRequest != null) { mLastSessionCreationRequest.dump(pw, indent); } } - public void startScan() { - if (mIsScanning) { + private void updateScanningState(@ScanningState int scanningState) { + if (mScanningState == scanningState) { return; } - mIsScanning = true; - mUserRecord.mHandler.sendMessage(PooledLambda.obtainMessage( - UserHandler::updateDiscoveryPreferenceOnHandler, mUserRecord.mHandler)); - } - public void stopScan() { - if (!mIsScanning) { - return; - } - mIsScanning = false; - mUserRecord.mHandler.sendMessage(PooledLambda.obtainMessage( - UserHandler::updateDiscoveryPreferenceOnHandler, mUserRecord.mHandler)); + mScanningState = scanningState; + + mUserRecord.mHandler.sendMessage( + obtainMessage( + UserHandler::updateDiscoveryPreferenceOnHandler, mUserRecord.mHandler)); } @Override @@ -3103,6 +3222,13 @@ class MediaRouter2ServiceImpl { buildCompositeDiscoveryPreference( activeRouterRecords, areManagersScanning, activelyScanningPackages); + Slog.i( + TAG, + TextUtils.formatSimple( + "Updating composite discovery preference | preference: %s, active" + + " routers: %s", + newPreference, activelyScanningPackages)); + if (updateScanningOnUserRecord(service, activelyScanningPackages, newPreference)) { updateDiscoveryPreferenceForProviders(activelyScanningPackages); } @@ -3152,7 +3278,7 @@ class MediaRouter2ServiceImpl { for (RouterRecord activeRouterRecord : activeRouterRecords) { RouteDiscoveryPreference preference = activeRouterRecord.mDiscoveryPreference; preferredFeatures.addAll(preference.getPreferredFeatures()); - if (preference.shouldPerformActiveScan()) { + if (activeRouterRecord.isActivelyScanning()) { activeScan = true; activelyScanningPackages.add(activeRouterRecord.mPackageName); } @@ -3175,33 +3301,40 @@ class MediaRouter2ServiceImpl { private static List<RouterRecord> getIndividuallyActiveRouters( MediaRouter2ServiceImpl service, List<RouterRecord> allRouterRecords) { if (!Flags.disableScreenOffBroadcastReceiver() - && !service.mPowerManager.isInteractive()) { + && !service.mPowerManager.isInteractive() + && !Flags.enableScreenOffScanning()) { return Collections.emptyList(); } return allRouterRecords.stream() .filter( record -> - service.mActivityManager.getPackageImportance( - record.mPackageName) - <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING) + isPackageImportanceSufficientForScanning( + service, record.mPackageName) + || record.mScanningState + == SCANNING_STATE_SCANNING_FULL) .collect(Collectors.toList()); } private static boolean areManagersScanning( MediaRouter2ServiceImpl service, List<ManagerRecord> managerRecords) { if (!Flags.disableScreenOffBroadcastReceiver() - && !service.mPowerManager.isInteractive()) { + && !service.mPowerManager.isInteractive() + && !Flags.enableScreenOffScanning()) { return false; } - return managerRecords.stream() - .anyMatch( - manager -> - manager.mIsScanning - && service.mActivityManager.getPackageImportance( - manager.mOwnerPackageName) - <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING); + return managerRecords.stream().anyMatch(manager -> + (manager.mScanningState == SCANNING_STATE_WHILE_INTERACTIVE + && isPackageImportanceSufficientForScanning(service, + manager.mOwnerPackageName)) + || manager.mScanningState == SCANNING_STATE_SCANNING_FULL); + } + + private static boolean isPackageImportanceSufficientForScanning( + MediaRouter2ServiceImpl service, String packageName) { + return service.mActivityManager.getPackageImportance(packageName) + <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING; } private MediaRoute2Provider findProvider(@Nullable String providerId) { diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 7dd13142f52e..6af3480989fa 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -43,6 +43,7 @@ import android.media.IMediaRouterClient; import android.media.IMediaRouterService; import android.media.MediaRoute2Info; import android.media.MediaRouter; +import android.media.MediaRouter2.ScanningState; import android.media.MediaRouterClientState; import android.media.RemoteDisplayState; import android.media.RemoteDisplayState.RemoteDisplayInfo; @@ -439,6 +440,13 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override + public void updateScanningStateWithRouter2( + IMediaRouter2 router, @ScanningState int scanningState) { + mService2.updateScanningState(router, scanningState); + } + + // Binder call + @Override public void setDiscoveryRequestWithRouter2(IMediaRouter2 router, RouteDiscoveryPreference request) { mService2.setDiscoveryRequestWithRouter2(router, request); @@ -574,14 +582,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override - public void startScan(IMediaRouter2Manager manager) { - mService2.startScan(manager); - } - - // Binder call - @Override - public void stopScan(IMediaRouter2Manager manager) { - mService2.stopScan(manager); + public void updateScanningState( + IMediaRouter2Manager manager, @ScanningState int scanningState) { + mService2.updateScanningState(manager, scanningState); } // Binder call |