diff --git a/Android.bp b/Android.bp index 3405ee3912e5e..f99abefc35b5f 100644 --- a/Android.bp +++ b/Android.bp @@ -484,8 +484,9 @@ java_defaults { "media/java/android/media/IMediaResourceMonitor.aidl", "media/java/android/media/IMediaRoute2Provider.aidl", "media/java/android/media/IMediaRoute2ProviderClient.aidl", - "media/java/android/media/IMediaRouterClient.aidl", + "media/java/android/media/IMediaRouter2Client.aidl", "media/java/android/media/IMediaRouter2Manager.aidl", + "media/java/android/media/IMediaRouterClient.aidl", "media/java/android/media/IMediaRouterService.aidl", "media/java/android/media/IMediaScannerListener.aidl", "media/java/android/media/IMediaScannerService.aidl", diff --git a/media/java/android/media/IMediaRoute2Provider.aidl b/media/java/android/media/IMediaRoute2Provider.aidl index b6fbf26cce4a0..4bd57106febe8 100644 --- a/media/java/android/media/IMediaRoute2Provider.aidl +++ b/media/java/android/media/IMediaRoute2Provider.aidl @@ -16,6 +16,7 @@ package android.media; +import android.content.Intent; import android.media.IMediaRoute2ProviderClient; /** @@ -23,5 +24,6 @@ import android.media.IMediaRoute2ProviderClient; */ oneway interface IMediaRoute2Provider { void registerClient(IMediaRoute2ProviderClient client); - void selectRoute(int uid, String id); + void selectRoute(IMediaRoute2ProviderClient client, int uid, String id); + void notifyControlRequestSent(IMediaRoute2ProviderClient client, String id, in Intent request); } diff --git a/media/java/android/media/IMediaRouter2Client.aidl b/media/java/android/media/IMediaRouter2Client.aidl new file mode 100644 index 0000000000000..774d6a764f322 --- /dev/null +++ b/media/java/android/media/IMediaRouter2Client.aidl @@ -0,0 +1,25 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +/** + * @hide + */ +oneway interface IMediaRouter2Client { + void notifyStateChanged(); + void notifyRestoreRoute(); +} diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 9e7bceb954a98..6d5b29a9fa32e 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -16,26 +16,35 @@ package android.media; -import android.media.IMediaRouterClient; +import android.content.Intent; +import android.media.IMediaRouter2Client; import android.media.IMediaRouter2Manager; +import android.media.IMediaRouterClient; +import android.media.MediaRoute2Info; import android.media.MediaRouterClientState; /** * {@hide} */ interface IMediaRouterService { + //TODO: Merge or remove methods when media router 2 is done. void registerClientAsUser(IMediaRouterClient client, String packageName, int userId); void unregisterClient(IMediaRouterClient client); MediaRouterClientState getState(IMediaRouterClient client); boolean isPlaybackActive(IMediaRouterClient client); - void setControlCategories(IMediaRouterClient client, in List categories); void setDiscoveryRequest(IMediaRouterClient client, int routeTypes, boolean activeScan); void setSelectedRoute(IMediaRouterClient client, String routeId, boolean explicit); void requestSetVolume(IMediaRouterClient client, String routeId, int volume); void requestUpdateVolume(IMediaRouterClient client, String routeId, int direction); + // Methods for media router 2 + void registerClient2AsUser(IMediaRouter2Client client, String packageName, int userId); + void unregisterClient2(IMediaRouter2Client client); + void sendControlRequest(IMediaRouter2Client client, in MediaRoute2Info route, in Intent request); + void setControlCategories(IMediaRouter2Client client, in List categories); + void registerManagerAsUser(IMediaRouter2Manager manager, String packageName, int userId); void unregisterManager(IMediaRouter2Manager manager); diff --git a/media/java/android/media/MediaRoute2Info.aidl b/media/java/android/media/MediaRoute2Info.aidl new file mode 100644 index 0000000000000..4811c4dc97c78 --- /dev/null +++ b/media/java/android/media/MediaRoute2Info.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +parcelable MediaRoute2Info; diff --git a/media/java/android/media/MediaRoute2ProviderInfo.java b/media/java/android/media/MediaRoute2ProviderInfo.java index 3ed5abc17f825..57d82d4e9d27c 100644 --- a/media/java/android/media/MediaRoute2ProviderInfo.java +++ b/media/java/android/media/MediaRoute2ProviderInfo.java @@ -137,9 +137,8 @@ public final class MediaRoute2ProviderInfo implements Parcelable { } public Builder(@NonNull MediaRoute2ProviderInfo descriptor) { - if (descriptor == null) { - throw new IllegalArgumentException("descriptor must not be null"); - } + Objects.requireNonNull(descriptor, "descriptor must not be null"); + mRoutes = new ArrayMap<>(descriptor.mRoutes); } diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java index fcd90b2399b56..30e0ef1fc1828 100644 --- a/media/java/android/media/MediaRoute2ProviderService.java +++ b/media/java/android/media/MediaRoute2ProviderService.java @@ -64,7 +64,16 @@ public abstract class MediaRoute2ProviderService extends Service { public abstract void onSelect(int uid, String routeId); /** - * Updates provider info from selected route and appliation. + * Called when sendControlRequest is called on a route of the provider. + * + * @param routeId The id of the target route + * @param request The media control request intent + */ + //TODO: Discuss what to use for request (e.g., Intent? Request class?) + public abstract void onControlRequest(String routeId, Intent request); + + /** + * Updates provider info from selected route and application. * * TODO: When provider descriptor is defined, this should update the descriptor correctly. * @@ -117,9 +126,16 @@ public abstract class MediaRoute2ProviderService extends Service { } @Override - public void selectRoute(int uid, String id) { + public void selectRoute(IMediaRoute2ProviderClient client, int uid, String id) { mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSelect, MediaRoute2ProviderService.this, uid, id)); } + + @Override + public void notifyControlRequestSent(IMediaRoute2ProviderClient client, String id, + Intent request) { + mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onControlRequest, + MediaRoute2ProviderService.this, id, request)); + } } } diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java index 5a89d8c82f3a0..3444e92779490 100644 --- a/media/java/android/media/MediaRouter.java +++ b/media/java/android/media/MediaRouter.java @@ -347,17 +347,6 @@ public class MediaRouter { return mDisplayService.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION); } - void setControlCategories(List categories) { - if (mClient != null) { - try { - mMediaRouterService.setControlCategories(mClient, - categories); - } catch (RemoteException ex) { - Log.e(TAG, "Unable to set control categories.", ex); - } - } - } - private void updatePresentationDisplays(int changedDisplayId) { final int count = mRoutes.size(); for (int i = 0; i < count; i++) { @@ -930,25 +919,6 @@ public class MediaRouter { return -1; } - //TODO: Remove @hide when it is ready. - //TODO: Provide pre-defined categories for app developers. - /** - * Sets control categories of the client application. - * Control categories can be used to filter out media routes - * that don't correspond with the client application. - * The only routes that match any of the categories will be shown on other applications. - * - * @hide - * @param categories Categories to set - */ - public void setControlCategories(@NonNull List categories) { - if (categories == null) { - throw new IllegalArgumentException("Categories must not be null"); - } - sStatic.setControlCategories(categories); - } - - /** * Select the specified route to use for output of the given media types. *

diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java new file mode 100644 index 0000000000000..d37a8321a9783 --- /dev/null +++ b/media/java/android/media/MediaRouter2.java @@ -0,0 +1,237 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.content.Context; +import android.content.Intent; +import android.os.Looper; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; + + +/** + * A new Media Router + * @hide + */ +public class MediaRouter2 { + private static final String TAG = "MediaRouter"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final Object sLock = new Object(); + + @GuardedBy("sLock") + private static MediaRouter2 sInstance; + + private Context mContext; + private final IMediaRouterService mMediaRouterService; + private List mCallbackRecords = new ArrayList<>(); + final String mPackageName; + + IMediaRouter2Client mClient; + + /** + * Gets an instance of the media router associated with the context. + */ + public static MediaRouter2 getInstance(@NonNull Context context) { + synchronized (sLock) { + if (sInstance == null) { + sInstance = new MediaRouter2(context); + } + return sInstance; + } + } + + private MediaRouter2(Context context) { + mContext = Objects.requireNonNull(context, "context must not be null"); + mMediaRouterService = IMediaRouterService.Stub.asInterface( + ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); + mPackageName = mContext.getPackageName(); + } + + /** + * Registers a callback to discover routes that match the selector and to receive events + * when they change. + */ + @MainThread + public void addCallback(@NonNull List controlCategories, @NonNull Executor executor, + @NonNull Callback callback) { + addCallback(controlCategories, executor, callback, 0); + } + + /** + * Registers a callback to discover routes that match the selector and to receive events + * when they change. + *

+ * If you add the same callback twice or more, the previous arguments will be overwritten + * with the new arguments. + *

+ */ + @MainThread + public void addCallback(@NonNull List controlCategories, @NonNull Executor executor, + @NonNull Callback callback, int flags) { + checkMainThread(); + + Objects.requireNonNull(controlCategories, "control categories must not be null"); + Objects.requireNonNull(executor, "executor must not be null"); + Objects.requireNonNull(callback, "callback must not be null"); + + if (mCallbackRecords.size() == 0) { + Client client = new Client(); + try { + mMediaRouterService.registerClient2AsUser(client, mPackageName, + UserHandle.myUserId()); + //TODO: We should merge control categories of callbacks. + mMediaRouterService.setControlCategories(client, controlCategories); + mClient = client; + } catch (RemoteException ex) { + Log.e(TAG, "Unable to register media router.", ex); + } + } + + final int index = findCallbackRecordIndex(callback); + CallbackRecord record; + if (index < 0) { + record = new CallbackRecord(callback); + mCallbackRecords.add(record); + } else { + record = mCallbackRecords.get(index); + } + record.mExecutor = executor; + record.mControlCategories = controlCategories; + record.mFlags = flags; + + //TODO: Check if we need an update. + } + + /** + * Removes the given callback. The callback will no longer receive events. + * If the callback has not been added or been removed already, it is ignored. + * @param callback the callback to remove. + * @see #addCallback + */ + @MainThread + public void removeCallback(@NonNull Callback callback) { + checkMainThread(); + + Objects.requireNonNull(callback, "callback must not be null"); + + final int index = findCallbackRecordIndex(callback); + if (index < 0) { + Log.w(TAG, "Ignoring to remove unknown callback. " + callback); + return; + } + mCallbackRecords.remove(index); + if (mCallbackRecords.size() == 0 && mClient != null) { + try { + mMediaRouterService.unregisterClient2(mClient); + } catch (RemoteException ex) { + Log.e(TAG, "Unable to unregister media router.", ex); + } + mClient = null; + } + } + + private int findCallbackRecordIndex(Callback callback) { + final int count = mCallbackRecords.size(); + for (int i = 0; i < count; i++) { + CallbackRecord callbackRecord = mCallbackRecords.get(i); + if (callbackRecord.mCallback == callback) { + return i; + } + } + return -1; + } + + /** + * Sends a media control request to be performed asynchronously by the route's destination. + * @param route the route that will receive the control request + * @param request the media control request + */ + //TODO: Discuss what to use for request (e.g., Intent? Request class?) + //TODO: Provide a way to obtain the result + public void sendControlRequest(@NonNull MediaRoute2Info route, @NonNull Intent request) { + Objects.requireNonNull(route, "route must not be null"); + Objects.requireNonNull(request, "request must not be null"); + + if (mClient != null) { + try { + mMediaRouterService.sendControlRequest(mClient, route, request); + } catch (RemoteException ex) { + Log.e(TAG, "Unable to send control request.", ex); + } + } + } + + void checkMainThread() { + Looper looper = Looper.myLooper(); + if (looper == null || looper != Looper.getMainLooper()) { + throw new IllegalStateException("the method must be called on the main thread"); + } + } + + /** + * Interface for receiving events about media routing changes. + */ + public static class Callback { + /** + * Called when a route is added. + */ + public void onRouteAdded(MediaRoute2Info routeInfo) {} + + /** + * Called when a route is changed. + */ + public void onRouteChanged(MediaRoute2Info routeInfo) {} + + /** + * Called when a route is removed. + */ + public void onRouteRemoved(MediaRoute2Info routeInfo) {} + } + + final class CallbackRecord { + public final Callback mCallback; + public Executor mExecutor; + public List mControlCategories; + public int mFlags; + + CallbackRecord(@NonNull Callback callback) { + mCallback = Objects.requireNonNull(callback, "callback must not be null"); + mControlCategories = Collections.emptyList(); + } + } + + class Client extends IMediaRouter2Client.Stub { + @Override + public void notifyStateChanged() throws RemoteException {} + + @Override + public void notifyRestoreRoute() throws RemoteException {} + } +} diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 0e16af36ff17a..5fcb684b70090 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -35,6 +35,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; @@ -62,14 +63,11 @@ public class MediaRouter2Manager { private List mProviders = Collections.emptyList(); /** - * Gets an instance of media router manager that controls media route of other apps. - * @param context - * @return + * Gets an instance of media router manager that controls media route of other applications. + * @return The media router manager instance for the context. */ public static MediaRouter2Manager getInstance(@NonNull Context context) { - if (context == null) { - throw new IllegalArgumentException("context must not be null"); - } + Objects.requireNonNull(context, "context must not be null"); synchronized (sLock) { if (sInstance == null) { sInstance = new MediaRouter2Manager(context); diff --git a/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java b/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java index c3c0c1149a446..21cb93d4f12e6 100644 --- a/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java +++ b/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java @@ -28,16 +28,23 @@ import java.util.Map; public class SampleMediaRoute2ProviderService extends MediaRoute2ProviderService { private static final String TAG = "SampleMediaRoute2Serv"; - public static final String ROUTE_ID1 = "route_id"; - public static final String ROUTE_NAME1 = "route_name"; + public static final String ROUTE_ID1 = "route_id1"; + public static final String ROUTE_NAME1 = "Sample Route 1"; + public static final String ROUTE_ID2 = "route_id2"; + public static final String ROUTE_NAME2 = "Sample Route 2"; + public static final String ACTION_REMOVE_ROUTE = + "com.android.mediarouteprovider.action_remove_route"; Map mRoutes = new HashMap<>(); private void initializeRoutes() { MediaRoute2Info route1 = new MediaRoute2Info.Builder(ROUTE_ID1, ROUTE_NAME1) .build(); + MediaRoute2Info route2 = new MediaRoute2Info.Builder(ROUTE_ID2, ROUTE_NAME2) + .build(); mRoutes.put(route1.getId(), route1); + mRoutes.put(route2.getId(), route2); } @Override @@ -57,6 +64,18 @@ public class SampleMediaRoute2ProviderService extends MediaRoute2ProviderService publishRoutes(); } + @Override + public void onControlRequest(String routeId, Intent request) { + if (ACTION_REMOVE_ROUTE.equals(request.getAction())) { + MediaRoute2Info route = mRoutes.get(routeId); + if (route != null) { + mRoutes.remove(routeId); + publishRoutes(); + mRoutes.put(routeId, route); + } + } + } + void publishRoutes() { MediaRoute2ProviderInfo info = new MediaRoute2ProviderInfo.Builder() .addRoutes(mRoutes.values()) diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java index 65d9d0638f098..41a76bf9dda9e 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java @@ -23,8 +23,9 @@ import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import android.content.Context; +import android.content.Intent; import android.media.MediaRoute2Info; -import android.media.MediaRouter; +import android.media.MediaRouter2; import android.media.MediaRouter2Manager; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; @@ -49,15 +50,19 @@ public class MediaRouterManagerTest { private static final int TARGET_UID = 109992; // Must be the same as SampleMediaRoute2ProviderService - public static final String ROUTE_ID1 = "route_id"; - public static final String ROUTE_NAME1 = "route_name"; + public static final String ROUTE_ID1 = "route_id1"; + public static final String ROUTE_NAME1 = "Sample Route 1"; + public static final String ROUTE_ID2 = "route_id2"; + public static final String ROUTE_NAME2 = "Sample Route 2"; + public static final String ACTION_REMOVE_ROUTE = + "com.android.mediarouteprovider.action_remove_route"; private static final int AWAIT_MS = 1000; private static final int TIMEOUT_MS = 5000; private Context mContext; private MediaRouter2Manager mManager; - private MediaRouter mRouter; + private MediaRouter2 mRouter; private Executor mExecutor; private static final List TEST_CONTROL_CATEGORIES = new ArrayList(); @@ -72,15 +77,15 @@ public class MediaRouterManagerTest { public void setUp() throws Exception { mContext = InstrumentationRegistry.getTargetContext(); mManager = MediaRouter2Manager.getInstance(mContext); - mRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE); + mRouter = MediaRouter2.getInstance(mContext); mExecutor = new ThreadPoolExecutor( 1, 20, 3, TimeUnit.SECONDS, new SynchronousQueue()); } - //TODO: onRouteChanged, onRouteRemoved must be tested + //TODO: Test onRouteChanged when it's properly implemented. @Test - public void testRouteAddedOnce() { + public void testRouteAdded() { MediaRouter2Manager.Callback mockCallback = mock(MediaRouter2Manager.Callback.class); mManager.addCallback(mExecutor, mockCallback); @@ -91,6 +96,30 @@ public class MediaRouterManagerTest { mManager.removeCallback(mockCallback); } + @Test + public void testRouteRemoved() { + MediaRouter2Manager.Callback mockCallback = mock(MediaRouter2Manager.Callback.class); + mManager.addCallback(mExecutor, mockCallback); + + MediaRouter2.Callback mockRouterCallback = mock(MediaRouter2.Callback.class); + + //TODO: Figure out a more proper way to test. + // (Control requests shouldn't be used in this way.) + InstrumentationRegistry.getInstrumentation().runOnMainSync( + (Runnable) () -> { + mRouter.addCallback(TEST_CONTROL_CATEGORIES, mExecutor, mockRouterCallback); + mRouter.sendControlRequest( + new MediaRoute2Info.Builder(ROUTE_ID2, ROUTE_NAME2).build(), + new Intent(ACTION_REMOVE_ROUTE)); + mRouter.removeCallback(mockRouterCallback); + } + ); + verify(mockCallback, timeout(TIMEOUT_MS)).onRouteRemoved(argThat( + (MediaRoute2Info info) -> + info.getId().equals(ROUTE_ID2) && info.getName().equals(ROUTE_NAME2))); + mManager.removeCallback(mockCallback); + } + @Test public void controlCategoryTest() throws Exception { final int uid = android.os.Process.myUid(); @@ -98,12 +127,19 @@ public class MediaRouterManagerTest { MediaRouter2Manager.Callback mockCallback = mock(MediaRouter2Manager.Callback.class); mManager.addCallback(mExecutor, mockCallback); + MediaRouter2.Callback mockRouterCallback = mock(MediaRouter2.Callback.class); + verify(mockCallback, after(AWAIT_MS).never()).onControlCategoriesChanged(uid, TEST_CONTROL_CATEGORIES); - mRouter.setControlCategories(TEST_CONTROL_CATEGORIES); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + (Runnable) () -> { + mRouter.addCallback(TEST_CONTROL_CATEGORIES, mExecutor, mockRouterCallback); + mRouter.removeCallback(mockRouterCallback); + } + ); verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce()) - .onControlCategoriesChanged(uid, TEST_CONTROL_CATEGORIES); + .onControlCategoriesChanged(uid, TEST_CONTROL_CATEGORIES); mManager.removeCallback(mockCallback); } diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java index 51c16c8f7d46a..2fd2d74f6e440 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java @@ -23,6 +23,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.media.IMediaRoute2Provider; import android.media.IMediaRoute2ProviderClient; +import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; import android.media.MediaRoute2ProviderService; import android.os.Handler; @@ -90,6 +91,13 @@ final class MediaRoute2ProviderProxy implements ServiceConnection { } } + public void sendControlRequest(MediaRoute2Info route, Intent request) { + if (mConnectionReady) { + mActiveConnection.sendControlRequest(route.getId(), request); + updateBinding(); + } + } + public MediaRoute2ProviderInfo getProviderInfo() { return mProviderInfo; } @@ -303,13 +311,27 @@ final class MediaRoute2ProviderProxy implements ServiceConnection { } public void selectRoute(int uid, String id) { + if (mClient == null) { + return; + } try { - mProvider.selectRoute(uid, id); + mProvider.selectRoute(mClient, uid, id); } catch (RemoteException ex) { Slog.e(TAG, "Failed to deliver request to set discovery mode.", ex); } } + public void sendControlRequest(String id, Intent request) { + if (mClient == null) { + return; + } + try { + mProvider.notifyControlRequestSent(mClient, id, request); + } catch (RemoteException ex) { + Slog.e(TAG, "Failed to deliver request to send control request.", ex); + } + } + @Override public void binderDied() { mHandler.post(() -> onConnectionDied(Connection.this)); diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 2ecc082c24c5b..98fbf757662cb 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -20,9 +20,11 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; +import android.media.IMediaRouter2Client; import android.media.IMediaRouter2Manager; -import android.media.IMediaRouterClient; +import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; import android.os.Binder; import android.os.Handler; @@ -68,7 +70,7 @@ class MediaRouter2ServiceImpl { mContext = context; } - public void registerClientAsUser(@NonNull IMediaRouterClient client, + public void registerClientAsUser(@NonNull IMediaRouter2Client client, @NonNull String packageName, int userId) { Objects.requireNonNull(client, "client must not be null"); @@ -89,7 +91,7 @@ class MediaRouter2ServiceImpl { } } - public void unregisterClient(@NonNull IMediaRouterClient client) { + public void unregisterClient(@NonNull IMediaRouter2Client client) { Objects.requireNonNull(client, "client must not be null"); final long token = Binder.clearCallingIdentity(); @@ -135,7 +137,23 @@ class MediaRouter2ServiceImpl { } } - public void setControlCategories(@NonNull IMediaRouterClient client, + public void sendControlRequest(@NonNull IMediaRouter2Client client, + @NonNull MediaRoute2Info route, @NonNull Intent request) { + Objects.requireNonNull(client, "client must not be null"); + Objects.requireNonNull(route, "route must not be null"); + Objects.requireNonNull(request, "request must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + sendControlRequestLocked(client, route, request); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + public void setControlCategories(@NonNull IMediaRouter2Client client, @Nullable List categories) { Objects.requireNonNull(client, "client must not be null"); @@ -195,7 +213,7 @@ class MediaRouter2ServiceImpl { } } - private void registerClientLocked(IMediaRouterClient client, + private void registerClientLocked(IMediaRouter2Client client, int uid, int pid, String packageName, int userId, boolean trusted) { final IBinder binder = client.asBinder(); ClientRecord clientRecord = mAllClientRecords.get(binder); @@ -223,7 +241,7 @@ class MediaRouter2ServiceImpl { } } - private void unregisterClientLocked(IMediaRouterClient client, boolean died) { + private void unregisterClientLocked(IMediaRouter2Client client, boolean died) { ClientRecord clientRecord = mAllClientRecords.remove(client.asBinder()); if (clientRecord != null) { UserRecord userRecord = clientRecord.mUserRecord; @@ -261,11 +279,11 @@ class MediaRouter2ServiceImpl { mAllManagerRecords.put(binder, managerRecord); //TODO: remove this when it's unnecessary - // Sends published routes to newly added manager + // Sends published routes to newly added manager. userRecord.mHandler.scheduleUpdateManagerState(); final int count = userRecord.mClientRecords.size(); - for (int i = 0; i < count; ++i) { + for (int i = 0; i < count; i++) { ClientRecord clientRecord = userRecord.mClientRecords.get(i); clientRecord.mUserRecord.mHandler.obtainMessage( UserHandler.MSG_UPDATE_CLIENT_USAGE, clientRecord).sendToTarget(); @@ -295,7 +313,7 @@ class MediaRouter2ServiceImpl { } } - private void setControlCategoriesLocked(IMediaRouterClient client, List categories) { + private void setControlCategoriesLocked(IMediaRouter2Client client, List categories) { final IBinder binder = client.asBinder(); ClientRecord clientRecord = mAllClientRecords.get(binder); @@ -306,6 +324,18 @@ class MediaRouter2ServiceImpl { } } + private void sendControlRequestLocked(IMediaRouter2Client client, MediaRoute2Info route, + Intent request) { + final IBinder binder = client.asBinder(); + ClientRecord clientRecord = mAllClientRecords.get(binder); + + if (clientRecord != null) { + Pair obj = new Pair<>(route, request); + clientRecord.mUserRecord.mHandler.obtainMessage( + UserHandler.MSG_SEND_CONTROL_REQUEST, obj).sendToTarget(); + } + } + private void initializeUserLocked(UserRecord userRecord) { if (DEBUG) { Slog.d(TAG, userRecord + ": Initialized"); @@ -345,14 +375,14 @@ class MediaRouter2ServiceImpl { final class ClientRecord implements IBinder.DeathRecipient { public final UserRecord mUserRecord; - public final IMediaRouterClient mClient; + public final IMediaRouter2Client mClient; public final int mUid; public final int mPid; public final String mPackageName; public final boolean mTrusted; public List mControlCategories; - ClientRecord(UserRecord userRecord, IMediaRouterClient client, + ClientRecord(UserRecord userRecord, IMediaRouter2Client client, int uid, int pid, String packageName, boolean trusted) { mUserRecord = userRecord; mClient = client; @@ -424,6 +454,7 @@ class MediaRouter2ServiceImpl { private static final int MSG_SELECT_REMOTE_ROUTE = 10; private static final int MSG_UPDATE_CLIENT_USAGE = 11; private static final int MSG_UPDATE_MANAGER_STATE = 12; + private static final int MSG_SEND_CONTROL_REQUEST = 13; private final WeakReference mServiceRef; private final UserRecord mUserRecord; @@ -468,6 +499,10 @@ class MediaRouter2ServiceImpl { updateManagerState(); break; } + case MSG_SEND_CONTROL_REQUEST: { + Pair obj = (Pair) msg.obj; + sendControlRequest(obj.first, obj.second); + } } } @@ -512,12 +547,19 @@ class MediaRouter2ServiceImpl { final int providerCount = mMediaProviders.size(); //TODO: should find proper provider (currently assumes a single provider) - for (int i = 0; i < providerCount; ++i) { + for (int i = 0; i < providerCount; i++) { mMediaProviders.get(i).setSelectedRoute(uid, routeId); } } } + private void sendControlRequest(MediaRoute2Info route, Intent request) { + final int providerCount = mMediaProviders.size(); + for (int i = 0; i < providerCount; i++) { + mMediaProviders.get(i).sendControlRequest(route, request); + } + } + private void scheduleUpdateManagerState() { if (!mManagerStateUpdateScheduled) { mManagerStateUpdateScheduled = true; diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index fade906df7c0c..aca6bccabee4c 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -30,9 +30,11 @@ import android.media.AudioRoutesInfo; import android.media.AudioSystem; import android.media.IAudioRoutesObserver; import android.media.IAudioService; +import android.media.IMediaRouter2Client; import android.media.IMediaRouter2Manager; import android.media.IMediaRouterClient; import android.media.IMediaRouterService; +import android.media.MediaRoute2Info; import android.media.MediaRouter; import android.media.MediaRouterClientState; import android.media.RemoteDisplayState; @@ -250,7 +252,6 @@ public final class MediaRouterService extends IMediaRouterService.Stub } finally { Binder.restoreCallingIdentity(token); } - mService2.registerClientAsUser(client, packageName, userId); } // Binder call @@ -268,7 +269,6 @@ public final class MediaRouterService extends IMediaRouterService.Stub } finally { Binder.restoreCallingIdentity(token); } - mService2.unregisterClient(client); } // Binder call @@ -310,12 +310,6 @@ public final class MediaRouterService extends IMediaRouterService.Stub } } - // Binder call - @Override - public void setControlCategories(IMediaRouterClient client, List categories) { - mService2.setControlCategories(client, categories); - } - // Binder call @Override public void setDiscoveryRequest(IMediaRouterClient client, @@ -416,6 +410,29 @@ public final class MediaRouterService extends IMediaRouterService.Stub } } + // Binder call + @Override + public void registerClient2AsUser(IMediaRouter2Client client, String packageName, int userId) { + final int uid = Binder.getCallingUid(); + if (!validatePackageName(uid, packageName)) { + throw new SecurityException("packageName must match the calling uid"); + } + mService2.registerClientAsUser(client, packageName, userId); + } + + // Binder call + @Override + public void unregisterClient2(IMediaRouter2Client client) { + mService2.unregisterClient(client); + } + + // Binder call + @Override + public void sendControlRequest(IMediaRouter2Client client, MediaRoute2Info route, + Intent request) { + mService2.sendControlRequest(client, route, request); + } + // Binder call @Override public void registerManagerAsUser(IMediaRouter2Manager manager, @@ -440,6 +457,12 @@ public final class MediaRouterService extends IMediaRouterService.Stub mService2.setRemoteRoute(manager, uid, routeId, explicit); } + // Binder call + @Override + public void setControlCategories(IMediaRouter2Client client, List categories) { + mService2.setControlCategories(client, categories); + } + void restoreBluetoothA2dp() { try { boolean a2dpOn;