Implement MediaRouter2-based InfoMediaManager

Bug: 192657812
Test: SettingsLibTests
Change-Id: I52b9e10d46777c5b1841d5a94247e7d32156a99a
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 7e27560..1ea155a6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -82,6 +82,13 @@
     private static final String TAG = "InfoMediaManager";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
+    /** Checked exception that signals the specified package is not present in the system. */
+    public static class PackageNotAvailableException extends Exception {
+        public PackageNotAvailableException(String message) {
+            super(message);
+        }
+    }
+
     protected String mPackageName;
     private MediaDevice mCurrentConnectedDevice;
     private final LocalBluetoothManager mBluetoothManager;
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
new file mode 100644
index 0000000..70956e9
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2023 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 com.android.settingslib.media;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.content.Context;
+import android.media.MediaRoute2Info;
+import android.media.MediaRouter2;
+import android.media.MediaRouter2.RoutingController;
+import android.media.MediaRouter2Manager;
+import android.media.RouteDiscoveryPreference;
+import android.media.RouteListingPreference;
+import android.media.RoutingSessionInfo;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/** Implements {@link InfoMediaManager} using {@link MediaRouter2}. */
+@SuppressLint("MissingPermission")
+public final class RouterInfoMediaManager extends InfoMediaManager {
+
+    private static final String TAG = "RouterInfoMediaManager";
+
+    private final MediaRouter2 mRouter;
+    private final MediaRouter2Manager mRouterManager;
+
+    private final Executor mExecutor = Executors.newSingleThreadExecutor();
+
+    private final RouteCallback mRouteCallback = new RouteCallback();
+    private final TransferCallback mTransferCallback = new TransferCallback();
+    private final ControllerCallback mControllerCallback = new ControllerCallback();
+    private final RouteListingPreferenceCallback mRouteListingPreferenceCallback =
+            new RouteListingPreferenceCallback();
+
+    // TODO: b/192657812 - Create factory method in InfoMediaManager to return
+    //      RouterInfoMediaManager or ManagerInfoMediaManager based on flag.
+    public RouterInfoMediaManager(
+            Context context,
+            String packageName,
+            Notification notification,
+            LocalBluetoothManager localBluetoothManager) throws PackageNotAvailableException {
+        super(context, packageName, notification, localBluetoothManager);
+
+        // TODO: b/291277292 - Change optional package name for a mandatory uid.
+        if (packageName == null) {
+            packageName = context.getPackageName();
+        }
+
+        mRouter = MediaRouter2.getInstance(context, packageName);
+
+        if (mRouter == null) {
+            throw new PackageNotAvailableException(
+                    "Package name " + packageName + " does not exist.");
+        }
+        mRouterManager = MediaRouter2Manager.getInstance(context);
+    }
+
+    @Override
+    protected void startScanOnRouter() {
+        mRouter.registerRouteCallback(mExecutor, mRouteCallback, RouteDiscoveryPreference.EMPTY);
+        mRouter.registerRouteListingPreferenceCallback(mExecutor, mRouteListingPreferenceCallback);
+        mRouter.registerTransferCallback(mExecutor, mTransferCallback);
+        mRouter.registerControllerCallback(mExecutor, mControllerCallback);
+        mRouter.startScan();
+    }
+
+    @Override
+    public void stopScan() {
+        mRouter.stopScan();
+        mRouter.unregisterControllerCallback(mControllerCallback);
+        mRouter.unregisterTransferCallback(mTransferCallback);
+        mRouter.unregisterRouteListingPreferenceCallback(mRouteListingPreferenceCallback);
+        mRouter.unregisterRouteCallback(mRouteCallback);
+    }
+
+    @Override
+    protected boolean connectDeviceWithoutPackageName(@NonNull MediaDevice device) {
+        if (device.mRouteInfo == null) {
+            return false;
+        }
+
+        RoutingController controller = mRouter.getSystemController();
+        mRouter.transfer(controller, device.mRouteInfo);
+        return true;
+    }
+
+    @Override
+    protected void transferToRoute(@NonNull MediaRoute2Info route) {
+        mRouter.transferTo(route);
+    }
+
+    @Override
+    protected void selectRoute(@NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info) {
+        RoutingController controller = getControllerForSession(info);
+        if (controller != null) {
+            controller.selectRoute(route);
+        }
+    }
+
+    @Override
+    protected void deselectRoute(@NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info) {
+        RoutingController controller = getControllerForSession(info);
+        if (controller != null) {
+            controller.deselectRoute(route);
+        }
+    }
+
+    @Override
+    protected void releaseSession(@NonNull RoutingSessionInfo sessionInfo) {
+        RoutingController controller = getControllerForSession(sessionInfo);
+        if (controller != null) {
+            controller.release();
+        }
+    }
+
+    @NonNull
+    @Override
+    protected List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo info) {
+        RoutingController controller = getControllerForSession(info);
+        if (controller == null) {
+            return Collections.emptyList();
+        }
+
+        // Filter out selected routes.
+        List<String> selectedRouteIds = controller.getRoutingSessionInfo().getSelectedRoutes();
+        return controller.getSelectableRoutes().stream()
+                .filter(route -> !selectedRouteIds.contains(route.getId()))
+                .collect(Collectors.toList());
+    }
+
+    @NonNull
+    @Override
+    protected List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo info) {
+        RoutingController controller = getControllerForSession(info);
+        if (controller == null) {
+            return Collections.emptyList();
+        }
+
+        return controller.getDeselectableRoutes();
+    }
+
+    @NonNull
+    @Override
+    protected List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo info) {
+        RoutingController controller = getControllerForSession(info);
+        if (controller == null) {
+            return Collections.emptyList();
+        }
+        return controller.getSelectedRoutes();
+    }
+
+    @Override
+    protected void setSessionVolume(@NonNull RoutingSessionInfo info, int volume) {
+        // TODO: b/291277292 - Implement MediaRouter2-based solution. Keeping MR2Manager call as
+        //      MR2 filters information by package name.
+        mRouterManager.setSessionVolume(info, volume);
+    }
+
+    @Override
+    protected void setRouteVolume(@NonNull MediaRoute2Info route, int volume) {
+        mRouter.setRouteVolume(route, volume);
+    }
+
+    @Nullable
+    @Override
+    protected RouteListingPreference getRouteListingPreference() {
+        return mRouter.getRouteListingPreference();
+    }
+
+    @NonNull
+    @Override
+    protected List<RoutingSessionInfo> getRemoteSessions() {
+        // TODO: b/291277292 - Implement MediaRouter2-based solution. Keeping MR2Manager call as
+        //      MR2 filters information by package name.
+        return mRouterManager.getRemoteSessions();
+    }
+
+    @NonNull
+    @Override
+    protected List<RoutingSessionInfo> getRoutingSessionsForPackage() {
+        return mRouter.getControllers().stream()
+                .map(RoutingController::getRoutingSessionInfo)
+                .collect(Collectors.toList());
+    }
+
+    @Nullable
+    @Override
+    protected RoutingSessionInfo getRoutingSessionById(@NonNull String sessionId) {
+        // TODO: b/291277292 - Implement MediaRouter2-based solution. Keeping MR2Manager calls as
+        //      MR2 filters information by package name.
+
+        for (RoutingSessionInfo sessionInfo : getRemoteSessions()) {
+            if (TextUtils.equals(sessionInfo.getId(), sessionId)) {
+                return sessionInfo;
+            }
+        }
+
+        RoutingSessionInfo systemSession = mRouterManager.getSystemRoutingSession(null);
+        return TextUtils.equals(systemSession.getId(), sessionId) ? systemSession : null;
+    }
+
+    @NonNull
+    @Override
+    protected List<MediaRoute2Info> getAllRoutes() {
+        return mRouter.getAllRoutes();
+    }
+
+    @NonNull
+    @Override
+    protected List<MediaRoute2Info> getAvailableRoutesFromRouter() {
+        return mRouter.getRoutes();
+    }
+
+    @NonNull
+    @Override
+    protected List<MediaRoute2Info> getTransferableRoutes(@NonNull String packageName) {
+        List<MediaRoute2Info> transferableRoutes = new ArrayList<>();
+
+        List<RoutingController> controllers = mRouter.getControllers();
+        RoutingController activeController = controllers.get(controllers.size() - 1);
+        RoutingSessionInfo sessionInfo = activeController.getRoutingSessionInfo();
+        List<MediaRoute2Info> routes = mRouter.getRoutes();
+
+        for (MediaRoute2Info route : routes) {
+            boolean isCrossDeviceTransfer = sessionInfo.isSystemSession() ^ route.isSystemRoute();
+
+            // Always show remote routes if transfer is local -> remote or viceversa regardless of
+            // whether route is in transferable routes list.
+            if (sessionInfo.getTransferableRoutes().contains(route.getId())
+                    || isCrossDeviceTransfer) {
+                transferableRoutes.add(route);
+            }
+        }
+
+        return transferableRoutes;
+    }
+
+    @Nullable
+    private RoutingController getControllerForSession(@NonNull RoutingSessionInfo sessionInfo) {
+        return mRouter.getController(sessionInfo.getId());
+    }
+
+    private final class RouteCallback extends MediaRouter2.RouteCallback {
+        @Override
+        public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {
+            refreshDevices();
+        }
+
+        @Override
+        public void onPreferredFeaturesChanged(@NonNull List<String> preferredFeatures) {
+            refreshDevices();
+        }
+    }
+
+    private final class TransferCallback extends MediaRouter2.TransferCallback {
+        @Override
+        public void onTransfer(
+                @NonNull RoutingController oldController,
+                @NonNull RoutingController newController) {
+            rebuildDeviceList();
+            notifyCurrentConnectedDeviceChanged();
+        }
+
+        @Override
+        public void onTransferFailure(@NonNull MediaRoute2Info requestedRoute) {
+            // Do nothing.
+        }
+
+        @Override
+        public void onStop(@NonNull RoutingController controller) {
+            refreshDevices();
+        }
+
+        @Override
+        public void onRequestFailed(int reason) {
+            dispatchOnRequestFailed(reason);
+        }
+    }
+
+    private final class ControllerCallback extends MediaRouter2.ControllerCallback {
+        @Override
+        public void onControllerUpdated(@NonNull RoutingController controller) {
+            refreshDevices();
+        }
+    }
+
+    private final class RouteListingPreferenceCallback
+            extends MediaRouter2.RouteListingPreferenceCallback {
+        @Override
+        public void onRouteListingPreferenceChanged(@Nullable RouteListingPreference preference) {
+            notifyRouteListingPreferenceUpdated(preference);
+            refreshDevices();
+        }
+    }
+}