MediaRouter: add volume control functionality into MR2

Add requestSetVolume and requestUpdateVolume into
MediaRouter2 and MediaRouter2Manager to enable users to control volume
of routes.

This CL also includes test cleanup for readability.

Bug: 142298160
Test: atest mediaroutertest
Change-Id: Iebb396c921517ec1a7677f90baa119ab647b5689
diff --git a/media/java/android/media/IMediaRoute2Provider.aidl b/media/java/android/media/IMediaRoute2Provider.aidl
index f132cef..66764c7 100644
--- a/media/java/android/media/IMediaRoute2Provider.aidl
+++ b/media/java/android/media/IMediaRoute2Provider.aidl
@@ -27,4 +27,6 @@
     void selectRoute(String packageName, String id);
     void unselectRoute(String packageName, String id);
     void notifyControlRequestSent(String id, in Intent request);
+    void requestSetVolume(String id, int volume);
+    void requestUpdateVolume(String id, int delta);
 }
diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl
index 81213b9..7b7a34e 100644
--- a/media/java/android/media/IMediaRouterService.aidl
+++ b/media/java/android/media/IMediaRouterService.aidl
@@ -46,6 +46,8 @@
     void registerClient2(IMediaRouter2Client client, String packageName);
     void unregisterClient2(IMediaRouter2Client client);
     void sendControlRequest(IMediaRouter2Client client, in MediaRoute2Info route, in Intent request);
+    void requestSetVolume2(IMediaRouter2Client client, in MediaRoute2Info route, int volume);
+    void requestUpdateVolume2(IMediaRouter2Client client, in MediaRoute2Info route, int direction);
     /**
      * Changes the selected route of the client.
      *
@@ -66,4 +68,9 @@
      */
     void selectClientRoute2(IMediaRouter2Manager manager, String packageName,
             in @nullable MediaRoute2Info route);
+
+    void requestSetVolume2Manager(IMediaRouter2Manager manager,
+            in MediaRoute2Info route, int volume);
+    void requestUpdateVolume2Manager(IMediaRouter2Manager manager,
+            in MediaRoute2Info route, int direction);
 }
diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java
index e8e0f82..58deff2 100644
--- a/media/java/android/media/MediaRoute2ProviderService.java
+++ b/media/java/android/media/MediaRoute2ProviderService.java
@@ -81,6 +81,20 @@
     public abstract void onControlRequest(String routeId, Intent request);
 
     /**
+     * Called when requestSetVolume is called on a route of the provider
+     * @param routeId the id of the route
+     * @param volume the target volume
+     */
+    public abstract void onSetVolume(String routeId, int volume);
+
+    /**
+     * Called when requestUpdateVolume is called on a route of the provider
+     * @param routeId id of the route
+     * @param delta the delta to add to the current volume
+     */
+    public abstract void onUpdateVolume(String routeId, int delta);
+
+    /**
      * Updates provider info and publishes routes
      */
     public final void setProviderInfo(MediaRoute2ProviderInfo info) {
@@ -130,5 +144,17 @@
             mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onControlRequest,
                     MediaRoute2ProviderService.this, id, request));
         }
+
+        @Override
+        public void requestSetVolume(String id, int volume) {
+            mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSetVolume,
+                    MediaRoute2ProviderService.this, id, volume));
+        }
+
+        @Override
+        public void requestUpdateVolume(String id, int delta) {
+            mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onUpdateVolume,
+                    MediaRoute2ProviderService.this, id, delta));
+        }
     }
 }
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index ed35ef6..ee61373 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -265,6 +265,54 @@
         }
     }
 
+    /**
+     * Requests a volume change for the route asynchronously.
+     * <p>
+     * It may have no effect if the route is currently not selected.
+     * </p>
+     *
+     * @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax}.
+     */
+    public void requestSetVolume(@NonNull MediaRoute2Info route, int volume) {
+        Objects.requireNonNull(route, "route must not be null");
+
+        Client client;
+        synchronized (sLock) {
+            client = mClient;
+        }
+        if (client != null) {
+            try {
+                mMediaRouterService.requestSetVolume2(client, route, volume);
+            } catch (RemoteException ex) {
+                Log.e(TAG, "Unable to send control request.", ex);
+            }
+        }
+    }
+
+    /**
+     * Requests an incremental volume update  for the route asynchronously.
+     * <p>
+     * It may have no effect if the route is currently not selected.
+     * </p>
+     *
+     * @param delta The delta to add to the current volume.
+     */
+    public void requestUpdateVolume(@NonNull MediaRoute2Info route, int delta) {
+        Objects.requireNonNull(route, "route must not be null");
+
+        Client client;
+        synchronized (sLock) {
+            client = mClient;
+        }
+        if (client != null) {
+            try {
+                mMediaRouterService.requestUpdateVolume2(client, route, delta);
+            } catch (RemoteException ex) {
+                Log.e(TAG, "Unable to send control request.", ex);
+            }
+        }
+    }
+
     @GuardedBy("mCallbackRecords")
     private int findCallbackRecordIndexLocked(Callback callback) {
         final int count = mCallbackRecords.size();
@@ -321,10 +369,21 @@
             if (!route.supportsControlCategory(controlCategories)) {
                 continue;
             }
+            MediaRoute2Info preRoute = findRouteById(route.getId());
+            if (!route.equals(preRoute)) {
+                notifyRouteChanged(route);
+            }
             outRoutes.add(route);
         }
     }
 
+    MediaRoute2Info findRouteById(String id) {
+        for (MediaRoute2Info route : mRoutes) {
+            if (route.getId().equals(id)) return route;
+        }
+        return null;
+    }
+
     void notifyRouteListChanged(List<MediaRoute2Info> routes) {
         for (CallbackRecord record: mCallbackRecords) {
             record.mExecutor.execute(
@@ -332,10 +391,18 @@
         }
     }
 
+    void notifyRouteChanged(MediaRoute2Info route) {
+        for (CallbackRecord record: mCallbackRecords) {
+            record.mExecutor.execute(
+                    () -> record.mCallback.onRouteChanged(route));
+        }
+    }
+
     /**
      * Interface for receiving events about media routing changes.
      */
     public static class Callback {
+        //TODO: clean up these callbacks
         /**
          * Called when a route is added.
          */
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index 0b64569..4f2a295 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -234,6 +234,54 @@
         }
     }
 
+    /**
+     * Requests a volume change for the route asynchronously.
+     * <p>
+     * It may have no effect if the route is currently not selected.
+     * </p>
+     *
+     * @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax}.
+     */
+    public void requestSetVolume(@NonNull MediaRoute2Info route, int volume) {
+        Objects.requireNonNull(route, "route must not be null");
+
+        Client client;
+        synchronized (sLock) {
+            client = mClient;
+        }
+        if (client != null) {
+            try {
+                mMediaRouterService.requestSetVolume2Manager(client, route, volume);
+            } catch (RemoteException ex) {
+                Log.e(TAG, "Unable to send control request.", ex);
+            }
+        }
+    }
+
+    /**
+     * Requests an incremental volume update  for the route asynchronously.
+     * <p>
+     * It may have no effect if the route is currently not selected.
+     * </p>
+     *
+     * @param delta The delta to add to the current volume.
+     */
+    public void requestUpdateVolume(@NonNull MediaRoute2Info route, int delta) {
+        Objects.requireNonNull(route, "route must not be null");
+
+        Client client;
+        synchronized (sLock) {
+            client = mClient;
+        }
+        if (client != null) {
+            try {
+                mMediaRouterService.requestUpdateVolume2Manager(client, route, delta);
+            } catch (RemoteException ex) {
+                Log.e(TAG, "Unable to send control request.", ex);
+            }
+        }
+    }
+
     int findProviderIndex(MediaRoute2ProviderInfo provider) {
         final int count = mProviders.size();
         for (int i = 0; i < count; i++) {
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 8d39a93..680c879 100644
--- a/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java
+++ b/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java
@@ -130,6 +130,33 @@
         }
     }
 
+    @Override
+    public void onSetVolume(String routeId, int volume) {
+        MediaRoute2Info route = mRoutes.get(routeId);
+        if (route == null) {
+            return;
+        }
+        volume = Math.min(volume, Math.max(0, route.getVolumeMax()));
+        mRoutes.put(routeId, new MediaRoute2Info.Builder(route)
+                .setVolume(volume)
+                .build());
+        publishRoutes();
+    }
+
+    @Override
+    public void onUpdateVolume(String routeId, int delta) {
+        MediaRoute2Info route = mRoutes.get(routeId);
+        if (route == null) {
+            return;
+        }
+        int volume = route.getVolume() + delta;
+        volume = Math.min(volume, Math.max(0, route.getVolumeMax()));
+        mRoutes.put(routeId, new MediaRoute2Info.Builder(route)
+                .setVolume(volume)
+                .build());
+        publishRoutes();
+    }
+
     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 da832ac..ca43d04 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java
@@ -47,8 +47,10 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -190,17 +192,8 @@
         MediaRouter2Manager.Callback mockCallback = mock(MediaRouter2Manager.Callback.class);
         mManager.registerCallback(mExecutor, mockCallback);
 
-        MediaRouter2.Callback mockRouterCallback = mock(MediaRouter2.Callback.class);
-
-        mRouter2.setControlCategories(CONTROL_CATEGORIES_SPECIAL);
-        mRouter2.registerCallback(mExecutor, mockRouterCallback);
-        mRouter2.unregisterCallback(mockRouterCallback);
-
-        verify(mockCallback, timeout(TIMEOUT_MS))
-                .onRoutesChanged(argThat(routes -> routes.size() > 0));
-
         Map<String, MediaRoute2Info> routes =
-                createRouteMap(mManager.getAvailableRoutes(mPackageName));
+                waitAndGetRoutesWithManager(CONTROL_CATEGORIES_SPECIAL);
 
         Assert.assertEquals(1, routes.size());
         Assert.assertNotNull(routes.get(ROUTE_ID_SPECIAL_CATEGORY));
@@ -214,12 +207,10 @@
     @Test
     public void testGetRoutes() throws Exception {
         MediaRouter2.Callback mockCallback = mock(MediaRouter2.Callback.class);
-
-        mRouter2.setControlCategories(CONTROL_CATEGORIES_SPECIAL);
         mRouter2.registerCallback(mExecutor, mockCallback);
-        verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce())
-                .onRoutesChanged(argThat(routes -> routes.size() > 0));
-        Map<String, MediaRoute2Info> routes = createRouteMap(mRouter2.getRoutes());
+
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(CONTROL_CATEGORIES_SPECIAL);
+
         Assert.assertEquals(1, routes.size());
         Assert.assertNotNull(routes.get(ROUTE_ID_SPECIAL_CATEGORY));
 
@@ -228,51 +219,40 @@
 
     @Test
     public void testOnRouteSelected() throws Exception {
-        MediaRouter2.Callback mockRouterCallback = mock(MediaRouter2.Callback.class);
+        MediaRouter2.Callback routerCallback = new MediaRouter2.Callback();
         MediaRouter2Manager.Callback managerCallback = mock(MediaRouter2Manager.Callback.class);
 
         mManager.registerCallback(mExecutor, managerCallback);
-        mRouter2.setControlCategories(CONTROL_CATEGORIES_ALL);
-        mRouter2.registerCallback(mExecutor, mockRouterCallback);
+        mRouter2.registerCallback(mExecutor, routerCallback);
 
-        verify(managerCallback, timeout(TIMEOUT_MS))
-                .onRoutesChanged(argThat(routes -> routes.size() > 0));
-
-        Map<String, MediaRoute2Info> routes =
-                createRouteMap(mManager.getAvailableRoutes(mPackageName));
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(CONTROL_CATEGORIES_ALL);
 
         MediaRoute2Info routeToSelect = routes.get(ROUTE_ID1);
-        mManager.selectRoute(mPackageName, routeToSelect);
-
         assertNotNull(routeToSelect);
+
+        mManager.selectRoute(mPackageName, routeToSelect);
         verify(managerCallback, timeout(TIMEOUT_MS))
                 .onRouteAdded(argThat(route -> route.equals(routeToSelect)));
 
+        mRouter2.unregisterCallback(routerCallback);
         mManager.unregisterCallback(managerCallback);
-        mRouter2.unregisterCallback(mockRouterCallback);
     }
 
     /**
      * Tests selecting and unselecting routes of a single provider.
      */
     @Test
-    public void testSingleProviderSelect() {
+    public void testSingleProviderSelect() throws Exception {
         MediaRouter2Manager.Callback managerCallback = mock(MediaRouter2Manager.Callback.class);
         MediaRouter2.Callback routerCallback = mock(MediaRouter2.Callback.class);
 
         mManager.registerCallback(mExecutor, managerCallback);
-        mRouter2.setControlCategories(CONTROL_CATEGORIES_ALL);
         mRouter2.registerCallback(mExecutor, routerCallback);
 
-        verify(managerCallback, timeout(TIMEOUT_MS))
-                .onRoutesChanged(argThat(routes -> routes.size() > 0));
-
-        Map<String, MediaRoute2Info> routes =
-                createRouteMap(mManager.getAvailableRoutes(mPackageName));
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(CONTROL_CATEGORIES_ALL);
 
         mManager.selectRoute(mPackageName, routes.get(ROUTE_ID1));
-        verify(managerCallback, timeout(TIMEOUT_MS)
-        )
+        verify(managerCallback, timeout(TIMEOUT_MS))
                 .onRouteChanged(argThat(routeInfo -> TextUtils.equals(ROUTE_ID1, routeInfo.getId())
                         && TextUtils.equals(routeInfo.getClientPackageName(), mPackageName)));
 
@@ -291,14 +271,67 @@
     }
 
     @Test
-    public void testVolumeHandling() {
+    public void testControlVolumeWithRouter() throws Exception {
         MediaRouter2.Callback mockCallback = mock(MediaRouter2.Callback.class);
 
-        mRouter2.setControlCategories(CONTROL_CATEGORIES_ALL);
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(CONTROL_CATEGORIES_ALL);
         mRouter2.registerCallback(mExecutor, mockCallback);
+
+        MediaRoute2Info volRoute = routes.get(ROUTE_ID_VARIABLE_VOLUME);
+        int originalVolume = volRoute.getVolume();
+        int deltaVolume = (originalVolume == volRoute.getVolumeMax() ? -1 : 1);
+        int targetVolume = originalVolume + deltaVolume;
+
+        mRouter2.requestSetVolume(volRoute, targetVolume);
         verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce())
-                .onRoutesChanged(argThat(routes -> routes.size() > 0));
-        Map<String, MediaRoute2Info> routes = createRouteMap(mRouter2.getRoutes());
+                .onRouteChanged(argThat(route ->
+                        route.getId().equals(volRoute.getId())
+                                && route.getVolume() == targetVolume));
+
+        mRouter2.requestUpdateVolume(volRoute, -deltaVolume);
+        verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce())
+                .onRouteChanged(argThat(route ->
+                        route.getId().equals(volRoute.getId())
+                                && route.getVolume() == originalVolume));
+
+        mRouter2.unregisterCallback(mockCallback);
+    }
+
+    @Test
+    public void testControlVolumeWithManager() throws Exception {
+        MediaRouter2Manager.Callback managerCallback = mock(MediaRouter2Manager.Callback.class);
+        MediaRouter2.Callback mockCallback = mock(MediaRouter2.Callback.class);
+
+        mManager.registerCallback(mExecutor, managerCallback);
+        mRouter2.registerCallback(mExecutor, mockCallback);
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(CONTROL_CATEGORIES_ALL);
+
+        MediaRoute2Info volRoute = routes.get(ROUTE_ID_VARIABLE_VOLUME);
+        int originalVolume = volRoute.getVolume();
+        int deltaVolume = (originalVolume == volRoute.getVolumeMax() ? -1 : 1);
+        int targetVolume = originalVolume + deltaVolume;
+
+        mManager.requestSetVolume(volRoute, targetVolume);
+        verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce())
+                .onRouteChanged(argThat(route ->
+                        route.getId().equals(volRoute.getId())
+                                && route.getVolume() == targetVolume));
+
+        mManager.requestUpdateVolume(volRoute, -deltaVolume);
+        verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce())
+                .onRouteChanged(argThat(route ->
+                        route.getId().equals(volRoute.getId())
+                                && route.getVolume() == originalVolume));
+
+        mRouter2.unregisterCallback(mockCallback);
+        mManager.unregisterCallback(managerCallback);
+    }
+
+    @Test
+    public void testVolumeHandling() throws Exception {
+        MediaRouter2.Callback mockCallback = mock(MediaRouter2.Callback.class);
+        mRouter2.registerCallback(mExecutor, mockCallback);
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(CONTROL_CATEGORIES_ALL);
 
         MediaRoute2Info fixedVolumeRoute = routes.get(ROUTE_ID_FIXED_VOLUME);
         MediaRoute2Info variableVolumeRoute = routes.get(ROUTE_ID_VARIABLE_VOLUME);
@@ -310,6 +343,48 @@
         mRouter2.unregisterCallback(mockCallback);
     }
 
+    Map<String, MediaRoute2Info> waitAndGetRoutes(List<String> controlCategories) throws Exception {
+        CountDownLatch latch = new CountDownLatch(1);
+        MediaRouter2.Callback callback = new MediaRouter2.Callback() {
+            @Override
+            public void onRoutesChanged(List<MediaRoute2Info> routes) {
+                if (routes.size() > 0) latch.countDown();
+            }
+        };
+        mRouter2.setControlCategories(controlCategories);
+        mRouter2.registerCallback(mExecutor, callback);
+        try {
+            latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            return createRouteMap(mRouter2.getRoutes());
+        } finally {
+            mRouter2.unregisterCallback(callback);
+        }
+    }
+
+    Map<String, MediaRoute2Info> waitAndGetRoutesWithManager(List<String> controlCategories)
+            throws Exception {
+        CountDownLatch latch = new CountDownLatch(1);
+
+        // Dummy callback is required to send control category info.
+        MediaRouter2.Callback routerCallback = new MediaRouter2.Callback();
+        MediaRouter2Manager.Callback managerCallback = new MediaRouter2Manager.Callback() {
+            @Override
+            public void onRoutesChanged(List<MediaRoute2Info> routes) {
+                if (routes.size() > 0) latch.countDown();
+            }
+        };
+        mManager.registerCallback(mExecutor, managerCallback);
+        mRouter2.setControlCategories(controlCategories);
+        mRouter2.registerCallback(mExecutor, routerCallback);
+        try {
+            latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            return createRouteMap(mManager.getAvailableRoutes(mPackageName));
+        } finally {
+            mRouter2.unregisterCallback(routerCallback);
+            mManager.unregisterCallback(managerCallback);
+        }
+    }
+
     // Helper for getting routes easily
     static Map<String, MediaRoute2Info> createRouteMap(List<MediaRoute2Info> routes) {
         Map<String, MediaRoute2Info> routeMap = new HashMap<>();
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java
index e753a7b..95eacd4 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java
@@ -105,6 +105,20 @@
         }
     }
 
+    public void requestSetVolume(MediaRoute2Info route, int volume) {
+        if (mConnectionReady) {
+            mActiveConnection.requestSetVolume(route.getId(), volume);
+            updateBinding();
+        }
+    }
+
+    public void requestUpdateVolume(MediaRoute2Info route, int delta) {
+        if (mConnectionReady) {
+            mActiveConnection.requestUpdateVolume(route.getId(), delta);
+            updateBinding();
+        }
+    }
+
     @Nullable
     public MediaRoute2ProviderInfo getProviderInfo() {
         return mProviderInfo;
@@ -340,6 +354,22 @@
             }
         }
 
+        public void requestSetVolume(String routeId, int volume) {
+            try {
+                mProvider.requestSetVolume(routeId, volume);
+            } catch (RemoteException ex) {
+                Slog.e(TAG, "Failed to deliver request to request set volume.", ex);
+            }
+        }
+
+        public void requestUpdateVolume(String routeId, int delta) {
+            try {
+                mProvider.requestUpdateVolume(routeId, delta);
+            } catch (RemoteException ex) {
+                Slog.e(TAG, "Failed to deliver request to request update volume.", 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 668f2be..74d59ac 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -198,6 +198,34 @@
         }
     }
 
+    public void requestSetVolume2(IMediaRouter2Client client, MediaRoute2Info route, int volume) {
+        Objects.requireNonNull(client, "client must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                requestSetVolumeLocked(client, route, volume);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void requestUpdateVolume2(IMediaRouter2Client client, MediaRoute2Info route, int delta) {
+        Objects.requireNonNull(client, "client must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                requestUpdateVolumeLocked(client, route, delta);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     public void selectClientRoute2(@NonNull IMediaRouter2Manager manager,
             String packageName, @Nullable MediaRoute2Info route) {
         final long token = Binder.clearCallingIdentity();
@@ -210,6 +238,37 @@
         }
     }
 
+    public void requestSetVolume2Manager(IMediaRouter2Manager manager,
+            MediaRoute2Info route, int volume) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                requestSetVolumeLocked(manager, route, volume);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void requestUpdateVolume2Manager(IMediaRouter2Manager manager,
+            MediaRoute2Info route, int delta) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                requestUpdateVolumeLocked(manager, route, delta);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+
     public void registerClient(@NonNull IMediaRouterClient client, @NonNull String packageName) {
         Objects.requireNonNull(client, "client must not be null");
 
@@ -362,6 +421,30 @@
         }
     }
 
+    private void requestSetVolumeLocked(IMediaRouter2Client client, MediaRoute2Info route,
+            int volume) {
+        final IBinder binder = client.asBinder();
+        ClientRecord clientRecord = mAllClientRecords.get(binder);
+
+        if (clientRecord != null) {
+            clientRecord.mUserRecord.mHandler.sendMessage(
+                    obtainMessage(UserHandler::requestSetVolume,
+                            clientRecord.mUserRecord.mHandler, route, volume));
+        }
+    }
+
+    private void requestUpdateVolumeLocked(IMediaRouter2Client client, MediaRoute2Info route,
+            int delta) {
+        final IBinder binder = client.asBinder();
+        ClientRecord clientRecord = mAllClientRecords.get(binder);
+
+        if (clientRecord != null) {
+            clientRecord.mUserRecord.mHandler.sendMessage(
+                    obtainMessage(UserHandler::requestUpdateVolume,
+                            clientRecord.mUserRecord.mHandler, route, delta));
+        }
+    }
+
     private void registerManagerLocked(IMediaRouter2Manager manager,
             int uid, int pid, String packageName, int userId, boolean trusted) {
         final IBinder binder = manager.asBinder();
@@ -424,6 +507,31 @@
         }
     }
 
+    private void requestSetVolumeLocked(IMediaRouter2Manager manager, MediaRoute2Info route,
+            int volume) {
+        final IBinder binder = manager.asBinder();
+        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+        if (managerRecord != null) {
+            managerRecord.mUserRecord.mHandler.sendMessage(
+                    obtainMessage(UserHandler::requestSetVolume,
+                            managerRecord.mUserRecord.mHandler, route, volume));
+        }
+    }
+
+    private void requestUpdateVolumeLocked(IMediaRouter2Manager manager, MediaRoute2Info route,
+            int delta) {
+        final IBinder binder = manager.asBinder();
+        ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+        if (managerRecord != null) {
+            managerRecord.mUserRecord.mHandler.sendMessage(
+                    obtainMessage(UserHandler::requestUpdateVolume,
+                            managerRecord.mUserRecord.mHandler, route, delta));
+        }
+    }
+
+
     private void initializeUserLocked(UserRecord userRecord) {
         if (DEBUG) {
             Slog.d(TAG, userRecord + ": Initialized");
@@ -679,6 +787,20 @@
             }
         }
 
+        private void requestSetVolume(MediaRoute2Info route, int volume) {
+            final MediaRoute2ProviderProxy provider = findProvider(route.getProviderId());
+            if (provider != null) {
+                provider.requestSetVolume(route, volume);
+            }
+        }
+
+        private void requestUpdateVolume(MediaRoute2Info route, int delta) {
+            final MediaRoute2ProviderProxy provider = findProvider(route.getProviderId());
+            if (provider != null) {
+                provider.requestUpdateVolume(route, delta);
+            }
+        }
+
         private void scheduleUpdateProviderInfos() {
             if (!mProviderInfosUpdateScheduled) {
                 mProviderInfosUpdateScheduled = true;
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 796a25d..afd92f6 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -499,6 +499,32 @@
         mService2.setControlCategories2(client, categories);
     }
 
+    // Binder call
+    @Override
+    public void requestSetVolume2(IMediaRouter2Client client, MediaRoute2Info route, int volume) {
+        mService2.requestSetVolume2(client, route, volume);
+    }
+
+    // Binder call
+    @Override
+    public void requestUpdateVolume2(IMediaRouter2Client client, MediaRoute2Info route, int delta) {
+        mService2.requestUpdateVolume2(client, route, delta);
+    }
+
+    // Binder call
+    @Override
+    public void requestSetVolume2Manager(IMediaRouter2Manager manager,
+            MediaRoute2Info route, int volume) {
+        mService2.requestSetVolume2Manager(manager, route, volume);
+    }
+
+    // Binder call
+    @Override
+    public void requestUpdateVolume2Manager(IMediaRouter2Manager manager,
+            MediaRoute2Info route, int delta) {
+        mService2.requestUpdateVolume2Manager(manager, route, delta);
+    }
+
     void restoreBluetoothA2dp() {
         try {
             boolean a2dpOn;