MediaRouter: Let provider release route controller

This CL lets MediaRouteProvider request a client
to release a route controller.

It can be used to support "Stop" in the output switcher.

In order to do that, we add a new message being used by
new client and new service.
For old clients, the service sends provider descriptor,
which contains a "disabled" route descriptor, which
eventually make the client unselect the route.

Bug: 163095048
Test: Run support v7 demos, cast to a sample provider,
tap "Stop" in the output switcher and confirm that onRouteUnselected is called.
The demos are built without MediaTransferReceiver not to use media router2.
CLINET_VERSION_3 and CLIENT_VERSION_4 are both tested as well.

Change-Id: Ieb2e9e529e90188466f6d91fe9d20ca3e9f665aa
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
index ea8b6d7..4b90f65 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
@@ -479,6 +479,8 @@
 
         SessionRecord sessionRecord = new SessionRecord(controller, REQUEST_ID_NONE,
                 sessionFlags, clientRecord);
+        //TODO: Reconsider the logic if dynamic grouping is enabled for clients < CLIENT_VERSION_4
+        sessionRecord.mRouteId = routeId;
 
         String sessionId = assignSessionId(sessionRecord);
         mSessionIdMap.put(controllerId, sessionId);
@@ -547,7 +549,7 @@
     private static class DynamicGroupRouteControllerProxy
             extends DynamicGroupRouteController {
         private final String mRouteId;
-        private final RouteController mRouteController;
+        final RouteController mRouteController;
 
         DynamicGroupRouteControllerProxy(String routeId, RouteController routeController) {
             mRouteId = routeId;
@@ -633,6 +635,8 @@
         private boolean mIsReleased;
         private RoutingSessionInfo mSessionInfo;
         String mSessionId;
+        // The ID of the route describing the session.
+        String mRouteId;
 
         SessionRecord(DynamicGroupRouteController controller, long requestId, int flags) {
             this(controller, requestId, flags, null);
@@ -697,6 +701,7 @@
 
             RoutingSessionInfo.Builder builder = new RoutingSessionInfo.Builder(sessionInfo);
             if (groupRoute != null) {
+                mRouteId = groupRoute.getId();
                 builder.setName(groupRoute.getName())
                         .setVolume(groupRoute.getVolume())
                         .setVolumeMax(groupRoute.getVolumeMax())
@@ -768,8 +773,22 @@
                 }
 
                 if (shouldUnselect) {
-                    mController.onUnselect(MediaRouter.UNSELECT_REASON_STOPPED);
-                    mController.onRelease();
+                    if ((mFlags & SESSION_FLAG_MR2) == 0) {
+                        // Let the client release the controller
+                        ClientRecord clientRecord = mClientRecord.get();
+                        if (clientRecord != null) {
+                            RouteController controller = mController;
+                            if (mController instanceof DynamicGroupRouteControllerProxy) {
+                                controller = ((DynamicGroupRouteControllerProxy) mController)
+                                        .mRouteController;
+                            }
+                            mServiceImpl.requestReleaseController(clientRecord,
+                                    controller, mRouteId);
+                        }
+                    } else {
+                        mController.onUnselect(MediaRouter.UNSELECT_REASON_STOPPED);
+                        mController.onRelease();
+                    }
                 }
                 mIsReleased = true;
                 notifySessionReleased(mSessionId);
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProviderProtocol.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProviderProtocol.java
index 5aa2914..27305d4 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProviderProtocol.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProviderProtocol.java
@@ -235,6 +235,13 @@
      */
     public static final int SERVICE_MSG_DYNAMIC_ROUTE_DESCRIPTORS_CHANGED = 7;
 
+    /** (service v3) / (client v4)
+     * Request to release a route controller. (unsolicited event)
+     * - arg1    : reserved(0)
+     * - arg2    : controllerId
+     */
+    public static final int SERVICE_MSG_RELEASE_CONTROLLER = 8;
+
     public static final String SERVICE_DATA_ERROR = "error";
 
     /*
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProviderService.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProviderService.java
index e3ba1d8..9ff3b77 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProviderService.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProviderService.java
@@ -51,6 +51,7 @@
 import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_GENERIC_FAILURE;
 import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_GENERIC_SUCCESS;
 import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_REGISTERED;
+import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_RELEASE_CONTROLLER;
 import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_VERSION_CURRENT;
 import static androidx.mediarouter.media.MediaRouteProviderProtocol.isValidRemoteMessenger;
 import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_UNKNOWN;
@@ -68,6 +69,7 @@
 import android.os.Message;
 import android.os.Messenger;
 import android.os.RemoteException;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -1079,7 +1081,6 @@
         }
     }
 
-    //TODO: We may need to change version number
     @RequiresApi(api = Build.VERSION_CODES.R)
     static class MediaRouteProviderServiceImplApi30 extends MediaRouteProviderServiceImplBase {
         MediaRoute2ProviderServiceAdapter mMR2ProviderServiceAdapter;
@@ -1120,6 +1121,49 @@
             mMR2ProviderServiceAdapter.setProviderDescriptor(descriptor);
         }
 
+        void requestReleaseController(ClientRecord client,
+                RouteController controller, String routeId) {
+            if (client.mVersion >= CLIENT_VERSION_4) {
+                int controllerId = client.findControllerIdByController(controller);
+                if (controllerId < 0) {
+                    Log.w(TAG, "requestReleaseController: Can't find the controller."
+                            + " route ID=" + routeId);
+                    return;
+                }
+                sendReply(client.mMessenger, SERVICE_MSG_RELEASE_CONTROLLER, 0, controllerId,
+                        null, null);
+                return;
+            }
+
+            // The below is a workaround to unselect the selected route of previous clients.
+            // The logic is based on the behavior that MediaRouter unselects its selected route if
+            // the route becomes disabled.
+            MediaRouteProviderDescriptor lastDescriptor =
+                    getService().getMediaRouteProvider().getDescriptor();
+            if (lastDescriptor == null) {
+                Log.w(TAG, "requestReleaseController: null provider descriptor found. "
+                        + "It shouldn't happen.");
+                return;
+            }
+
+            List<MediaRouteDescriptor> routes = new ArrayList<>();
+            for (MediaRouteDescriptor descriptor : lastDescriptor.getRoutes()) {
+                if (TextUtils.equals(descriptor.getId(), routeId)) {
+                    routes.add(new MediaRouteDescriptor.Builder(descriptor)
+                            .setEnabled(false).build());
+                } else {
+                    routes.add(descriptor);
+                }
+            }
+
+            MediaRouteProviderDescriptor providerDescriptor =
+                    new MediaRouteProviderDescriptor.Builder(lastDescriptor)
+                    .setRoutes(routes).build();
+            sendReply(client.mMessenger, SERVICE_MSG_DESCRIPTOR_CHANGED, 0, 0,
+                    createDescriptorBundleForClientVersion(providerDescriptor, client.mVersion),
+                    null);
+        }
+
         @Override
         MediaRouteProviderServiceImplBase.ClientRecord createClientRecord(
                 Messenger messenger, int version, String packageName) {
@@ -1207,6 +1251,12 @@
             public RouteController findControllerByRouteId(String routeId) {
                 return mRouteIdToControllerMap.get(routeId);
             }
+
+            public int findControllerIdByController(RouteController controller) {
+                int index = mControllers.indexOfValue(controller);
+                if (index < 0) return -1;
+                return mControllers.keyAt(index);
+            }
         }
     }
 }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
index 7469b77..39a66a0 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
@@ -2808,6 +2808,16 @@
             }
         }
 
+        @Override
+        public void releaseProviderController(@NonNull RegisteredMediaRouteProvider provider,
+                @NonNull RouteController controller) {
+            if (mSelectedRouteController == controller) {
+                selectRoute(chooseFallbackRoute(), UNSELECT_REASON_STOPPED);
+            }
+            //TODO: Maybe release a member route controller if the given controller is a member of
+            // the selected route.
+        }
+
         void updateProviderDescriptor(MediaRouteProvider providerInstance,
                 MediaRouteProviderDescriptor descriptor) {
             ProviderInfo provider = findProviderInfo(providerInstance);
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProvider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProvider.java
index 4386432..1182537 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProvider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProvider.java
@@ -50,6 +50,7 @@
 import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_GENERIC_FAILURE;
 import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_GENERIC_SUCCESS;
 import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_REGISTERED;
+import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_RELEASE_CONTROLLER;
 import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_VERSION_1;
 import static androidx.mediarouter.media.MediaRouteProviderProtocol.isValidRemoteMessenger;
 
@@ -95,6 +96,7 @@
     private boolean mBound;
     private Connection mActiveConnection;
     private boolean mConnectionReady;
+    private ControllerCallback mControllerCallback;
 
     public RegisteredMediaRouteProvider(Context context, ComponentName componentName) {
         super(context, new ProviderMetadata(componentName));
@@ -212,6 +214,10 @@
         }
     }
 
+    public void setControllerCallback(@Nullable ControllerCallback controllerCallback) {
+        mControllerCallback = controllerCallback;
+    }
+
     private void updateBinding() {
         if (shouldBind()) {
             bind();
@@ -377,6 +383,15 @@
         }
     }
 
+    void onConnectionRequestReleaseController(Connection connection, int controllerId) {
+        if (mActiveConnection == connection) {
+            ControllerConnection controller = findControllerById(controllerId);
+            if (mControllerCallback != null && controller instanceof RouteController) {
+                mControllerCallback.onRequestReleaseController(((RouteController) controller));
+            }
+        }
+    }
+
     private ControllerConnection findControllerById(int id) {
         for (ControllerConnection controller: mControllerConnections) {
             if (controller.getControllerId() == id) {
@@ -835,6 +850,10 @@
             }
         }
 
+        public void onRequestReleaseController(int controllerId) {
+            onConnectionRequestReleaseController(this, controllerId);
+        }
+
         @Override
         public void binderDied() {
             mPrivateHandler.post(new Runnable() {
@@ -1055,8 +1074,16 @@
                         Log.w(TAG, "No further information on the dynamic group controller");
                     }
                     break;
+
+                case SERVICE_MSG_RELEASE_CONTROLLER:
+                    connection.onRequestReleaseController(arg /* controllerId */);
+                    break;
             }
             return false;
         }
     }
+
+    interface ControllerCallback {
+        void onRequestReleaseController(RouteController controller);
+    }
 }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
index ab121f4..b952d01a 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
@@ -44,7 +44,7 @@
  */
 final class RegisteredMediaRouteProviderWatcher {
     private final Context mContext;
-    private final Callback mCallback;
+    final Callback mCallback;
     private final Handler mHandler;
     private final PackageManager mPackageManager;
 
@@ -121,6 +121,8 @@
             if (sourceIndex < 0) {
                 RegisteredMediaRouteProvider provider = new RegisteredMediaRouteProvider(
                         mContext, new ComponentName(serviceInfo.packageName, serviceInfo.name));
+                provider.setControllerCallback(
+                        controller -> mCallback.releaseProviderController(provider, controller));
                 provider.start();
                 mProviders.add(targetIndex++, provider);
                 mCallback.addProvider(provider);
@@ -138,6 +140,7 @@
                 RegisteredMediaRouteProvider provider = mProviders.get(i);
                 mCallback.removeProvider(provider);
                 mProviders.remove(provider);
+                provider.setControllerCallback(null);
                 provider.stop();
             }
         }
@@ -192,5 +195,7 @@
     public interface Callback {
         void addProvider(MediaRouteProvider provider);
         void removeProvider(MediaRouteProvider provider);
+        void releaseProviderController(@NonNull RegisteredMediaRouteProvider provider,
+                @NonNull MediaRouteProvider.RouteController controller);
     }
 }
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java
index e9aaa25..c671387 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java
@@ -142,7 +142,7 @@
                 mPlayer.updatePresentation();
             }
             mSessionManager.setPlayer(mPlayer);
-            if (reason == MediaRouter.UNSELECT_REASON_STOPPED) {
+            if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
                 mSessionManager.stop();
             } else {
                 mSessionManager.unsuspend();