Merge "Add support for wired routing" into main
diff --git a/services/core/java/com/android/server/media/AudioAttributesUtils.java b/services/core/java/com/android/server/media/AudioAttributesUtils.java
deleted file mode 100644
index 8cb334d..0000000
--- a/services/core/java/com/android/server/media/AudioAttributesUtils.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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.server.media;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.media.AudioAttributes;
-import android.media.AudioDeviceAttributes;
-import android.media.AudioDeviceInfo;
-import android.media.MediaRoute2Info;
-
-import com.android.media.flags.Flags;
-
-/* package */ final class AudioAttributesUtils {
-
-    /* package */ static final AudioAttributes ATTRIBUTES_MEDIA = new AudioAttributes.Builder()
-            .setUsage(AudioAttributes.USAGE_MEDIA)
-            .build();
-
-    private AudioAttributesUtils() {
-        // no-op to prevent instantiation.
-    }
-
-    @MediaRoute2Info.Type
-    /* package */ static int mapToMediaRouteType(
-            @NonNull AudioDeviceAttributes audioDeviceAttributes) {
-        if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
-            switch (audioDeviceAttributes.getType()) {
-                case AudioDeviceInfo.TYPE_HDMI_ARC:
-                    return MediaRoute2Info.TYPE_HDMI_ARC;
-                case AudioDeviceInfo.TYPE_HDMI_EARC:
-                    return MediaRoute2Info.TYPE_HDMI_EARC;
-            }
-        }
-        switch (audioDeviceAttributes.getType()) {
-            case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
-            case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
-                return MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
-            case AudioDeviceInfo.TYPE_WIRED_HEADSET:
-                return MediaRoute2Info.TYPE_WIRED_HEADSET;
-            case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
-                return MediaRoute2Info.TYPE_WIRED_HEADPHONES;
-            case AudioDeviceInfo.TYPE_DOCK:
-            case AudioDeviceInfo.TYPE_DOCK_ANALOG:
-                return MediaRoute2Info.TYPE_DOCK;
-            case AudioDeviceInfo.TYPE_HDMI:
-            case AudioDeviceInfo.TYPE_HDMI_ARC:
-            case AudioDeviceInfo.TYPE_HDMI_EARC:
-                return MediaRoute2Info.TYPE_HDMI;
-            case AudioDeviceInfo.TYPE_USB_DEVICE:
-                return MediaRoute2Info.TYPE_USB_DEVICE;
-            case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
-                return MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
-            case AudioDeviceInfo.TYPE_BLE_HEADSET:
-                return MediaRoute2Info.TYPE_BLE_HEADSET;
-            case AudioDeviceInfo.TYPE_HEARING_AID:
-                return MediaRoute2Info.TYPE_HEARING_AID;
-            default:
-                return MediaRoute2Info.TYPE_UNKNOWN;
-        }
-    }
-
-    /* package */ static boolean isDeviceOutputAttributes(
-            @Nullable AudioDeviceAttributes audioDeviceAttributes) {
-        if (audioDeviceAttributes == null) {
-            return false;
-        }
-
-        if (audioDeviceAttributes.getRole() != AudioDeviceAttributes.ROLE_OUTPUT) {
-            return false;
-        }
-
-        switch (audioDeviceAttributes.getType()) {
-            case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
-            case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
-            case AudioDeviceInfo.TYPE_WIRED_HEADSET:
-            case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
-            case AudioDeviceInfo.TYPE_DOCK:
-            case AudioDeviceInfo.TYPE_DOCK_ANALOG:
-            case AudioDeviceInfo.TYPE_HDMI:
-            case AudioDeviceInfo.TYPE_HDMI_ARC:
-            case AudioDeviceInfo.TYPE_HDMI_EARC:
-            case AudioDeviceInfo.TYPE_USB_DEVICE:
-                return true;
-            default:
-                return false;
-        }
-    }
-
-    /* package */ static boolean isBluetoothOutputAttributes(
-            @Nullable AudioDeviceAttributes audioDeviceAttributes) {
-        if (audioDeviceAttributes == null) {
-            return false;
-        }
-
-        if (audioDeviceAttributes.getRole() != AudioDeviceAttributes.ROLE_OUTPUT) {
-            return false;
-        }
-
-        switch (audioDeviceAttributes.getType()) {
-            case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
-            case AudioDeviceInfo.TYPE_BLE_HEADSET:
-            case AudioDeviceInfo.TYPE_BLE_SPEAKER:
-            case AudioDeviceInfo.TYPE_HEARING_AID:
-                return true;
-            default:
-                return false;
-        }
-    }
-
-}
diff --git a/services/core/java/com/android/server/media/AudioPoliciesBluetoothRouteController.java b/services/core/java/com/android/server/media/AudioPoliciesBluetoothRouteController.java
index 8bc69c2..a00999d 100644
--- a/services/core/java/com/android/server/media/AudioPoliciesBluetoothRouteController.java
+++ b/services/core/java/com/android/server/media/AudioPoliciesBluetoothRouteController.java
@@ -17,7 +17,6 @@
 package com.android.server.media;
 
 import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_AUDIO;
-import static android.bluetooth.BluetoothAdapter.STATE_CONNECTED;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -31,38 +30,37 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.media.AudioManager;
-import android.media.AudioSystem;
 import android.media.MediaRoute2Info;
 import android.os.UserHandle;
 import android.text.TextUtils;
+import android.util.Log;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
-import android.util.SparseIntArray;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
- * Controls bluetooth routes and provides selected route override.
+ * Maintains a list of connected {@link BluetoothDevice bluetooth devices} and allows their
+ * activation.
  *
- * <p>The controller offers similar functionality to {@link LegacyBluetoothRouteController} but does
- * not support routes selection logic. Instead, relies on external clients to make a decision
- * about currently selected route.
- *
- * <p>Selected route override should be used by {@link AudioManager} which is aware of Audio
- * Policies.
+ * <p>This class also serves as ground truth for assigning {@link MediaRoute2Info#getId() route ids}
+ * for bluetooth routes via {@link #getRouteIdForBluetoothAddress}.
  */
-/* package */ class AudioPoliciesBluetoothRouteController
-        implements BluetoothRouteController {
-    private static final String TAG = "APBtRouteController";
+// TODO: b/305199571 - Rename this class to remove the RouteController suffix, which causes
+// confusion with the BluetoothRouteController interface.
+/* package */ class AudioPoliciesBluetoothRouteController {
+    private static final String TAG = SystemMediaRoute2Provider.TAG;
 
     private static final String HEARING_AID_ROUTE_ID_PREFIX = "HEARING_AID_";
     private static final String LE_AUDIO_ROUTE_ID_PREFIX = "LE_AUDIO_";
@@ -75,11 +73,8 @@
     private final DeviceStateChangedReceiver mDeviceStateChangedReceiver =
             new DeviceStateChangedReceiver();
 
-    @NonNull
-    private final Map<String, BluetoothRouteInfo> mBluetoothRoutes = new HashMap<>();
-
-    @NonNull
-    private final SparseIntArray mVolumeMap = new SparseIntArray();
+    @NonNull private Map<String, BluetoothDevice> mAddressToBondedDevice = new HashMap<>();
+    @NonNull private final Map<String, BluetoothRouteInfo> mBluetoothRoutes = new HashMap<>();
 
     @NonNull
     private final Context mContext;
@@ -89,11 +84,6 @@
     private final BluetoothRouteController.BluetoothRoutesUpdatedListener mListener;
     @NonNull
     private final BluetoothProfileMonitor mBluetoothProfileMonitor;
-    @NonNull
-    private final AudioManager mAudioManager;
-
-    @Nullable
-    private BluetoothRouteInfo mSelectedBluetoothRoute;
 
     AudioPoliciesBluetoothRouteController(@NonNull Context context,
             @NonNull BluetoothAdapter bluetoothAdapter,
@@ -107,21 +97,12 @@
             @NonNull BluetoothAdapter bluetoothAdapter,
             @NonNull BluetoothProfileMonitor bluetoothProfileMonitor,
             @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) {
-        Objects.requireNonNull(context);
-        Objects.requireNonNull(bluetoothAdapter);
-        Objects.requireNonNull(bluetoothProfileMonitor);
-        Objects.requireNonNull(listener);
-
-        mContext = context;
-        mBluetoothAdapter = bluetoothAdapter;
-        mBluetoothProfileMonitor = bluetoothProfileMonitor;
-        mAudioManager = mContext.getSystemService(AudioManager.class);
-        mListener = listener;
-
-        updateBluetoothRoutes();
+        mContext = Objects.requireNonNull(context);
+        mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter);
+        mBluetoothProfileMonitor = Objects.requireNonNull(bluetoothProfileMonitor);
+        mListener = Objects.requireNonNull(listener);
     }
 
-    @Override
     public void start(UserHandle user) {
         mBluetoothProfileMonitor.start();
 
@@ -133,122 +114,63 @@
 
         IntentFilter deviceStateChangedIntentFilter = new IntentFilter();
 
-        deviceStateChangedIntentFilter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
         deviceStateChangedIntentFilter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
         deviceStateChangedIntentFilter.addAction(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
         deviceStateChangedIntentFilter.addAction(
                 BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
         deviceStateChangedIntentFilter.addAction(
                 BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
-        deviceStateChangedIntentFilter.addAction(
-                BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED);
 
         mContext.registerReceiverAsUser(mDeviceStateChangedReceiver, user,
                 deviceStateChangedIntentFilter, null, null);
+        updateBluetoothRoutes();
     }
 
-    @Override
     public void stop() {
         mContext.unregisterReceiver(mAdapterStateChangedReceiver);
         mContext.unregisterReceiver(mDeviceStateChangedReceiver);
     }
 
-    @Override
-    public boolean selectRoute(@Nullable String deviceAddress) {
-        synchronized (this) {
-            // Fetch all available devices in order to avoid race conditions with Bluetooth stack.
-            updateBluetoothRoutes();
-
-            if (deviceAddress == null) {
-                mSelectedBluetoothRoute = null;
-                return true;
-            }
-
-            BluetoothRouteInfo bluetoothRouteInfo = mBluetoothRoutes.get(deviceAddress);
-
-            if (bluetoothRouteInfo == null) {
-                Slog.w(TAG, "Cannot find bluetooth route for " + deviceAddress);
-                return false;
-            }
-
-            mSelectedBluetoothRoute = bluetoothRouteInfo;
-            setRouteConnectionState(mSelectedBluetoothRoute, STATE_CONNECTED);
-
-            updateConnectivityStateForDevicesInTheSameGroup();
-
-            return true;
-        }
+    @Nullable
+    public synchronized String getRouteIdForBluetoothAddress(@Nullable String address) {
+        BluetoothDevice bluetoothDevice = mAddressToBondedDevice.get(address);
+        // TODO: b/305199571 - Optimize the following statement to avoid creating the full
+        // MediaRoute2Info instance. We just need the id.
+        return bluetoothDevice != null
+                ? createBluetoothRoute(bluetoothDevice).mRoute.getId()
+                : null;
     }
 
-    /**
-     * Updates connectivity state for devices in the same devices group.
-     *
-     * <p>{@link BluetoothProfile#LE_AUDIO} and {@link BluetoothProfile#HEARING_AID} support
-     * grouping devices. Devices that belong to the same group should have the same routeId but
-     * different physical address.
-     *
-     * <p>In case one of the devices from the group is selected then other devices should also
-     * reflect this by changing their connectivity status to
-     * {@link MediaRoute2Info#CONNECTION_STATE_CONNECTED}.
-     */
-    private void updateConnectivityStateForDevicesInTheSameGroup() {
-        synchronized (this) {
-            for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) {
-                if (TextUtils.equals(btRoute.mRoute.getId(), mSelectedBluetoothRoute.mRoute.getId())
-                        && !TextUtils.equals(btRoute.mBtDevice.getAddress(),
-                        mSelectedBluetoothRoute.mBtDevice.getAddress())) {
-                    setRouteConnectionState(btRoute, STATE_CONNECTED);
-                }
-            }
-        }
-    }
-
-    @Override
-    public void transferTo(@Nullable String routeId) {
-        if (routeId == null) {
-            mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_AUDIO);
-            return;
-        }
-
-        BluetoothRouteInfo btRouteInfo = findBluetoothRouteWithRouteId(routeId);
+    public synchronized void activateBluetoothDeviceWithAddress(String address) {
+        BluetoothRouteInfo btRouteInfo = mBluetoothRoutes.get(address);
 
         if (btRouteInfo == null) {
-            Slog.w(TAG, "transferTo: Unknown route. ID=" + routeId);
+            Slog.w(TAG, "activateBluetoothDeviceWithAddress: Ignoring unknown address " + address);
             return;
         }
-
         mBluetoothAdapter.setActiveDevice(btRouteInfo.mBtDevice, ACTIVE_DEVICE_AUDIO);
     }
 
-    @Nullable
-    private BluetoothRouteInfo findBluetoothRouteWithRouteId(@Nullable String routeId) {
-        if (routeId == null) {
-            return null;
-        }
-        synchronized (this) {
-            for (BluetoothRouteInfo btRouteInfo : mBluetoothRoutes.values()) {
-                if (TextUtils.equals(btRouteInfo.mRoute.getId(), routeId)) {
-                    return btRouteInfo;
-                }
-            }
-        }
-        return null;
-    }
-
     private void updateBluetoothRoutes() {
         Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices();
 
-        if (bondedDevices == null) {
-            return;
-        }
-
         synchronized (this) {
             mBluetoothRoutes.clear();
-
-            // We need to query all available to BT stack devices in order to avoid inconsistency
-            // between external services, like, AndroidManager, and BT stack.
+            if (bondedDevices == null) {
+                // Bonded devices is null upon running into a BluetoothAdapter error.
+                Log.w(TAG, "BluetoothAdapter.getBondedDevices returned null.");
+                return;
+            }
+            // We don't clear bonded devices if we receive a null getBondedDevices result, because
+            // that probably means that the bluetooth stack ran into an issue. Not that all devices
+            // have been unpaired.
+            mAddressToBondedDevice =
+                    bondedDevices.stream()
+                            .collect(
+                                    Collectors.toMap(
+                                            BluetoothDevice::getAddress, Function.identity()));
             for (BluetoothDevice device : bondedDevices) {
-                if (isDeviceConnected(device)) {
+                if (device.isConnected()) {
                     BluetoothRouteInfo newBtRoute = createBluetoothRoute(device);
                     if (newBtRoute.mConnectedProfiles.size() > 0) {
                         mBluetoothRoutes.put(device.getAddress(), newBtRoute);
@@ -258,106 +180,51 @@
         }
     }
 
-    @VisibleForTesting
-        /* package */ boolean isDeviceConnected(@NonNull BluetoothDevice device) {
-        return device.isConnected();
-    }
-
-    @Nullable
-    @Override
-    public MediaRoute2Info getSelectedRoute() {
-        synchronized (this) {
-            if (mSelectedBluetoothRoute == null) {
-                return null;
-            }
-
-            return mSelectedBluetoothRoute.mRoute;
-        }
-    }
-
     @NonNull
-    @Override
-    public List<MediaRoute2Info> getTransferableRoutes() {
-        List<MediaRoute2Info> routes = getAllBluetoothRoutes();
-        synchronized (this) {
-            if (mSelectedBluetoothRoute != null) {
-                routes.remove(mSelectedBluetoothRoute.mRoute);
-            }
-        }
-        return routes;
-    }
-
-    @NonNull
-    @Override
-    public List<MediaRoute2Info> getAllBluetoothRoutes() {
+    public List<MediaRoute2Info> getAvailableBluetoothRoutes() {
         List<MediaRoute2Info> routes = new ArrayList<>();
-        List<String> routeIds = new ArrayList<>();
-
-        MediaRoute2Info selectedRoute = getSelectedRoute();
-        if (selectedRoute != null) {
-            routes.add(selectedRoute);
-            routeIds.add(selectedRoute.getId());
-        }
+        Set<String> routeIds = new HashSet<>();
 
         synchronized (this) {
             for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) {
-                // A pair of hearing aid devices or having the same hardware address
-                if (routeIds.contains(btRoute.mRoute.getId())) {
-                    continue;
+                // See createBluetoothRoute for info on why we do this.
+                if (routeIds.add(btRoute.mRoute.getId())) {
+                    routes.add(btRoute.mRoute);
                 }
-                routes.add(btRoute.mRoute);
-                routeIds.add(btRoute.mRoute.getId());
             }
         }
         return routes;
     }
 
-    @Override
-    public boolean updateVolumeForDevices(int devices, int volume) {
-        int routeType;
-        if ((devices & (AudioSystem.DEVICE_OUT_HEARING_AID)) != 0) {
-            routeType = MediaRoute2Info.TYPE_HEARING_AID;
-        } else if ((devices & (AudioManager.DEVICE_OUT_BLUETOOTH_A2DP
-                | AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES
-                | AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0) {
-            routeType = MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
-        } else if ((devices & (AudioManager.DEVICE_OUT_BLE_HEADSET)) != 0) {
-            routeType = MediaRoute2Info.TYPE_BLE_HEADSET;
-        } else {
-            return false;
-        }
-
-        synchronized (this) {
-            mVolumeMap.put(routeType, volume);
-            if (mSelectedBluetoothRoute == null
-                    || mSelectedBluetoothRoute.mRoute.getType() != routeType) {
-                return false;
-            }
-
-            mSelectedBluetoothRoute.mRoute =
-                    new MediaRoute2Info.Builder(mSelectedBluetoothRoute.mRoute)
-                            .setVolume(volume)
-                            .build();
-        }
-
-        notifyBluetoothRoutesUpdated();
-        return true;
-    }
-
     private void notifyBluetoothRoutesUpdated() {
         mListener.onBluetoothRoutesUpdated();
     }
 
+    /**
+     * Creates a new {@link BluetoothRouteInfo}, including its member {@link
+     * BluetoothRouteInfo#mRoute}.
+     *
+     * <p>The most important logic in this method is around the {@link MediaRoute2Info#getId() route
+     * id} assignment. In some cases we want to group multiple {@link BluetoothDevice bluetooth
+     * devices} as a single media route. For example, the left and right hearing aids get exposed as
+     * two different BluetoothDevice instances, but we want to show them as a single route. In this
+     * case, we assign the same route id to all "group" bluetooth devices (like left and right
+     * hearing aids), so that a single route is exposed for both of them.
+     *
+     * <p>Deduplication by id happens downstream because we need to be able to refer to all
+     * bluetooth devices individually, since the audio stack refers to a bluetooth device group by
+     * any of its member devices.
+     */
     private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device) {
         BluetoothRouteInfo
                 newBtRoute = new BluetoothRouteInfo();
         newBtRoute.mBtDevice = device;
-
-        String routeId = device.getAddress();
         String deviceName = device.getName();
         if (TextUtils.isEmpty(deviceName)) {
             deviceName = mContext.getResources().getText(R.string.unknownName).toString();
         }
+
+        String routeId = device.getAddress();
         int type = MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
         newBtRoute.mConnectedProfiles = new SparseBooleanArray();
         if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.A2DP, device)) {
@@ -365,7 +232,6 @@
         }
         if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.HEARING_AID, device)) {
             newBtRoute.mConnectedProfiles.put(BluetoothProfile.HEARING_AID, true);
-            // Intentionally assign the same ID for a pair of devices to publish only one of them.
             routeId = HEARING_AID_ROUTE_ID_PREFIX
                     + mBluetoothProfileMonitor.getGroupId(BluetoothProfile.HEARING_AID, device);
             type = MediaRoute2Info.TYPE_HEARING_AID;
@@ -377,66 +243,27 @@
             type = MediaRoute2Info.TYPE_BLE_HEADSET;
         }
 
-        // Current volume will be set when connected.
-        newBtRoute.mRoute = new MediaRoute2Info.Builder(routeId, deviceName)
-                .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO)
-                .addFeature(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK)
-                .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED)
-                .setDescription(mContext.getResources().getText(
-                        R.string.bluetooth_a2dp_audio_route_name).toString())
-                .setType(type)
-                .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
-                .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
-                .setAddress(device.getAddress())
-                .build();
+        // Note that volume is only relevant for active bluetooth routes, and those are managed via
+        // AudioManager.
+        newBtRoute.mRoute =
+                new MediaRoute2Info.Builder(routeId, deviceName)
+                        .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO)
+                        .addFeature(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK)
+                        .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED)
+                        .setDescription(
+                                mContext.getResources()
+                                        .getText(R.string.bluetooth_a2dp_audio_route_name)
+                                        .toString())
+                        .setType(type)
+                        .setAddress(device.getAddress())
+                        .build();
         return newBtRoute;
     }
 
-    private void setRouteConnectionState(@NonNull BluetoothRouteInfo btRoute,
-            @MediaRoute2Info.ConnectionState int state) {
-        if (btRoute == null) {
-            Slog.w(TAG, "setRouteConnectionState: route shouldn't be null");
-            return;
-        }
-        if (btRoute.mRoute.getConnectionState() == state) {
-            return;
-        }
-
-        MediaRoute2Info.Builder builder = new MediaRoute2Info.Builder(btRoute.mRoute)
-                .setConnectionState(state);
-        builder.setType(btRoute.getRouteType());
-
-
-
-        if (state == MediaRoute2Info.CONNECTION_STATE_CONNECTED) {
-            int currentVolume;
-            synchronized (this) {
-                currentVolume = mVolumeMap.get(btRoute.getRouteType(), 0);
-            }
-            builder.setVolume(currentVolume);
-        }
-
-        btRoute.mRoute = builder.build();
-    }
-
     private static class BluetoothRouteInfo {
         private BluetoothDevice mBtDevice;
         private MediaRoute2Info mRoute;
         private SparseBooleanArray mConnectedProfiles;
-
-        @MediaRoute2Info.Type
-        int getRouteType() {
-            // Let hearing aid profile have a priority.
-            if (mConnectedProfiles.get(BluetoothProfile.HEARING_AID, false)) {
-                return MediaRoute2Info.TYPE_HEARING_AID;
-            }
-
-            if (mConnectedProfiles.get(BluetoothProfile.LE_AUDIO, false)) {
-                return MediaRoute2Info.TYPE_BLE_HEADSET;
-            }
-
-            return MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
-        }
     }
 
     private class AdapterStateChangedReceiver extends BroadcastReceiver {
@@ -468,9 +295,6 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             switch (intent.getAction()) {
-                case BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED:
-                case BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED:
-                case BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED:
                 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
                 case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED:
                 case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED:
diff --git a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java
index 6bdfae2..246d68d 100644
--- a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java
+++ b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java
@@ -17,228 +17,596 @@
 package com.android.server.media;
 
 import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO;
-import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO;
 import static android.media.MediaRoute2Info.FEATURE_LOCAL_PLAYBACK;
-import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
-import static android.media.MediaRoute2Info.TYPE_DOCK;
-import static android.media.MediaRoute2Info.TYPE_HDMI;
-import static android.media.MediaRoute2Info.TYPE_HDMI_ARC;
-import static android.media.MediaRoute2Info.TYPE_HDMI_EARC;
-import static android.media.MediaRoute2Info.TYPE_USB_DEVICE;
-import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
-import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
 
+import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
 import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceCallback;
+import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
-import android.media.AudioRoutesInfo;
-import android.media.IAudioRoutesObserver;
-import android.media.IAudioService;
 import android.media.MediaRoute2Info;
-import android.os.RemoteException;
+import android.media.audiopolicy.AudioProductStrategy;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.Slog;
+import android.util.SparseArray;
 
 import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.media.BluetoothRouteController.NoOpBluetoothRouteController;
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
+/**
+ * Maintains a list of all available routes and supports transfers to any of them.
+ *
+ * <p>This implementation is intended for use in conjunction with {@link
+ * NoOpBluetoothRouteController}, as it manages bluetooth devices directly.
+ *
+ * <p>This implementation obtains and manages all routes via {@link AudioManager}, with the
+ * exception of {@link AudioManager#handleBluetoothActiveDeviceChanged inactive bluetooth} routes
+ * which are managed by {@link AudioPoliciesBluetoothRouteController}, which depends on the
+ * bluetooth stack (for example {@link BluetoothAdapter}.
+ */
+// TODO: b/305199571 - Rename this class to avoid the AudioPolicies prefix, which has been flagged
+// by the audio team as a confusing name.
 /* package */ final class AudioPoliciesDeviceRouteController implements DeviceRouteController {
-
-    private static final String TAG = "APDeviceRoutesController";
+    private static final String TAG = SystemMediaRoute2Provider.TAG;
 
     @NonNull
-    private final Context mContext;
-    @NonNull
-    private final AudioManager mAudioManager;
-    @NonNull
-    private final IAudioService mAudioService;
+    private static final AudioAttributes MEDIA_USAGE_AUDIO_ATTRIBUTES =
+            new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
 
     @NonNull
-    private final OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
-    @NonNull
-    private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver();
+    private static final SparseArray<SystemRouteInfo> AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO =
+            new SparseArray<>();
 
-    private int mDeviceVolume;
+    @NonNull private final Context mContext;
+    @NonNull private final AudioManager mAudioManager;
+    @NonNull private final Handler mHandler;
+    @NonNull private final OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
+    @NonNull private final AudioPoliciesBluetoothRouteController mBluetoothRouteController;
 
     @NonNull
-    private MediaRoute2Info mDeviceRoute;
-    @Nullable
-    private MediaRoute2Info mSelectedRoute;
+    private final Map<String, MediaRoute2InfoHolder> mRouteIdToAvailableDeviceRoutes =
+            new HashMap<>();
 
-    @VisibleForTesting
-    /* package */ AudioPoliciesDeviceRouteController(@NonNull Context context,
+    @NonNull private final AudioProductStrategy mStrategyForMedia;
+
+    @NonNull private final AudioDeviceCallback mAudioDeviceCallback = new AudioDeviceCallbackImpl();
+
+    @NonNull
+    private final AudioManager.OnDevicesForAttributesChangedListener
+            mOnDevicesForAttributesChangedListener = this::onDevicesForAttributesChangedListener;
+
+    @NonNull private MediaRoute2Info mSelectedRoute;
+
+    // TODO: b/305199571 - Support nullable btAdapter and strategyForMedia which, when null, means
+    // no support for transferring to inactive bluetooth routes and transferring to any routes
+    // respectively.
+    @RequiresPermission(
+            anyOf = {
+                Manifest.permission.MODIFY_AUDIO_ROUTING,
+                Manifest.permission.QUERY_AUDIO_STATE
+            })
+    /* package */ AudioPoliciesDeviceRouteController(
+            @NonNull Context context,
             @NonNull AudioManager audioManager,
-            @NonNull IAudioService audioService,
+            @NonNull Looper looper,
+            @NonNull AudioProductStrategy strategyForMedia,
+            @NonNull BluetoothAdapter btAdapter,
             @NonNull OnDeviceRouteChangedListener onDeviceRouteChangedListener) {
-        Objects.requireNonNull(context);
-        Objects.requireNonNull(audioManager);
-        Objects.requireNonNull(audioService);
-        Objects.requireNonNull(onDeviceRouteChangedListener);
-
-        mContext = context;
-        mOnDeviceRouteChangedListener = onDeviceRouteChangedListener;
-
-        mAudioManager = audioManager;
-        mAudioService = audioService;
-
-        AudioRoutesInfo newAudioRoutes = null;
-        try {
-            newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver);
-        } catch (RemoteException e) {
-            Slog.w(TAG, "Cannot connect to audio service to start listen to routes", e);
-        }
-
-        mDeviceRoute = createRouteFromAudioInfo(newAudioRoutes);
+        mContext = Objects.requireNonNull(context);
+        mAudioManager = Objects.requireNonNull(audioManager);
+        mHandler = new Handler(Objects.requireNonNull(looper));
+        mStrategyForMedia = Objects.requireNonNull(strategyForMedia);
+        mOnDeviceRouteChangedListener = Objects.requireNonNull(onDeviceRouteChangedListener);
+        mBluetoothRouteController =
+                new AudioPoliciesBluetoothRouteController(
+                        mContext, btAdapter, this::rebuildAvailableRoutesAndNotify);
+        // Just build routes but don't notify. The caller may not expect the listener to be invoked
+        // before this constructor has finished executing.
+        rebuildAvailableRoutes();
     }
 
+    @RequiresPermission(
+            anyOf = {
+                Manifest.permission.MODIFY_AUDIO_ROUTING,
+                Manifest.permission.QUERY_AUDIO_STATE
+            })
     @Override
-    public synchronized boolean selectRoute(@Nullable Integer type) {
-        if (type == null) {
-            mSelectedRoute = null;
-            return true;
-        }
+    public void start(UserHandle mUser) {
+        mBluetoothRouteController.start(mUser);
+        mAudioManager.registerAudioDeviceCallback(mAudioDeviceCallback, mHandler);
+        mAudioManager.addOnDevicesForAttributesChangedListener(
+                AudioRoutingUtils.ATTRIBUTES_MEDIA,
+                new HandlerExecutor(mHandler),
+                mOnDevicesForAttributesChangedListener);
+    }
 
-        if (!isDeviceRouteType(type)) {
-            return false;
-        }
-
-        mSelectedRoute = createRouteFromAudioInfo(type);
-        return true;
+    @RequiresPermission(
+            anyOf = {
+                Manifest.permission.MODIFY_AUDIO_ROUTING,
+                Manifest.permission.QUERY_AUDIO_STATE
+            })
+    @Override
+    public void stop() {
+        mAudioManager.removeOnDevicesForAttributesChangedListener(
+                mOnDevicesForAttributesChangedListener);
+        mAudioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
+        mBluetoothRouteController.stop();
+        mHandler.removeCallbacksAndMessages(/* token= */ null);
     }
 
     @Override
     @NonNull
     public synchronized MediaRoute2Info getSelectedRoute() {
-        if (mSelectedRoute != null) {
-            return mSelectedRoute;
-        }
-        return mDeviceRoute;
+        return mSelectedRoute;
     }
 
     @Override
+    @NonNull
+    public synchronized List<MediaRoute2Info> getAvailableRoutes() {
+        return mRouteIdToAvailableDeviceRoutes.values().stream()
+                .map(it -> it.mMediaRoute2Info)
+                .toList();
+    }
+
+    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
+    @Override
+    public synchronized void transferTo(@Nullable String routeId) {
+        if (routeId == null) {
+            // This should never happen: This branch should only execute when the matching bluetooth
+            // route controller is not the no-op one.
+            // TODO: b/305199571 - Make routeId non-null and remove this branch once we remove the
+            // legacy route controller implementations.
+            Slog.e(TAG, "Unexpected call to AudioPoliciesDeviceRouteController#transferTo(null)");
+            return;
+        }
+        MediaRoute2InfoHolder mediaRoute2InfoHolder = mRouteIdToAvailableDeviceRoutes.get(routeId);
+        if (mediaRoute2InfoHolder == null) {
+            Slog.w(TAG, "transferTo: Ignoring transfer request to unknown route id : " + routeId);
+            return;
+        }
+        if (mediaRoute2InfoHolder.mCorrespondsToInactiveBluetoothRoute) {
+            // By default, the last connected device is the active route so we don't need to apply a
+            // routing audio policy.
+            mBluetoothRouteController.activateBluetoothDeviceWithAddress(
+                    mediaRoute2InfoHolder.mMediaRoute2Info.getAddress());
+            mAudioManager.removePreferredDeviceForStrategy(mStrategyForMedia);
+        } else {
+            AudioDeviceAttributes attr =
+                    new AudioDeviceAttributes(
+                            AudioDeviceAttributes.ROLE_OUTPUT,
+                            mediaRoute2InfoHolder.mAudioDeviceInfoType,
+                            /* address= */ ""); // This is not a BT device, hence no address needed.
+            mAudioManager.setPreferredDeviceForStrategy(mStrategyForMedia, attr);
+        }
+    }
+
+    @RequiresPermission(
+            anyOf = {
+                Manifest.permission.MODIFY_AUDIO_ROUTING,
+                Manifest.permission.QUERY_AUDIO_STATE
+            })
+    @Override
     public synchronized boolean updateVolume(int volume) {
-        if (mDeviceVolume == volume) {
-            return false;
-        }
-
-        mDeviceVolume = volume;
-
-        if (mSelectedRoute != null) {
-            mSelectedRoute = new MediaRoute2Info.Builder(mSelectedRoute)
-                    .setVolume(volume)
-                    .build();
-        }
-
-        mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute)
-                .setVolume(volume)
-                .build();
-
+        // TODO: b/305199571 - Optimize so that we only update the volume of the selected route. We
+        // don't need to rebuild all available routes.
+        rebuildAvailableRoutesAndNotify();
         return true;
     }
 
-    @NonNull
-    private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) {
-        int type = TYPE_BUILTIN_SPEAKER;
+    @RequiresPermission(
+            anyOf = {
+                Manifest.permission.MODIFY_AUDIO_ROUTING,
+                Manifest.permission.QUERY_AUDIO_STATE
+            })
+    private void onDevicesForAttributesChangedListener(
+            AudioAttributes attributes, List<AudioDeviceAttributes> unusedAudioDeviceAttributes) {
+        if (attributes.getUsage() == AudioAttributes.USAGE_MEDIA) {
+            // We only care about the media usage. Ignore everything else.
+            rebuildAvailableRoutesAndNotify();
+        }
+    }
 
-        if (newRoutes != null) {
-            if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) {
-                type = TYPE_WIRED_HEADPHONES;
-            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
-                type = TYPE_WIRED_HEADSET;
-            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
-                type = TYPE_DOCK;
-            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) {
-                type = TYPE_HDMI;
-            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) {
-                type = TYPE_USB_DEVICE;
+    private synchronized void rebuildAvailableRoutesAndNotify() {
+        rebuildAvailableRoutes();
+        mOnDeviceRouteChangedListener.onDeviceRouteChanged();
+    }
+
+    @RequiresPermission(
+            anyOf = {
+                Manifest.permission.MODIFY_AUDIO_ROUTING,
+                Manifest.permission.QUERY_AUDIO_STATE
+            })
+    private synchronized void rebuildAvailableRoutes() {
+        List<AudioDeviceAttributes> attributesOfSelectedOutputDevices =
+                mAudioManager.getDevicesForAttributes(MEDIA_USAGE_AUDIO_ATTRIBUTES);
+        int selectedDeviceAttributesType;
+        if (attributesOfSelectedOutputDevices.isEmpty()) {
+            Slog.e(
+                    TAG,
+                    "Unexpected empty list of output devices for media. Using built-in speakers.");
+            selectedDeviceAttributesType = AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
+        } else {
+            if (attributesOfSelectedOutputDevices.size() > 1) {
+                Slog.w(
+                        TAG,
+                        "AudioManager.getDevicesForAttributes returned more than one element. Using"
+                                + " the first one.");
+            }
+            selectedDeviceAttributesType = attributesOfSelectedOutputDevices.get(0).getType();
+        }
+
+        AudioDeviceInfo[] audioDeviceInfos =
+                mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+        mRouteIdToAvailableDeviceRoutes.clear();
+        MediaRoute2InfoHolder newSelectedRouteHolder = null;
+        for (AudioDeviceInfo audioDeviceInfo : audioDeviceInfos) {
+            MediaRoute2Info mediaRoute2Info =
+                    createMediaRoute2InfoFromAudioDeviceInfo(audioDeviceInfo);
+            // Null means audioDeviceInfo is not a supported media output, like a phone's builtin
+            // earpiece. We ignore those.
+            if (mediaRoute2Info != null) {
+                int audioDeviceInfoType = audioDeviceInfo.getType();
+                MediaRoute2InfoHolder newHolder =
+                        MediaRoute2InfoHolder.createForAudioManagerRoute(
+                                mediaRoute2Info, audioDeviceInfoType);
+                mRouteIdToAvailableDeviceRoutes.put(mediaRoute2Info.getId(), newHolder);
+                if (selectedDeviceAttributesType == audioDeviceInfoType) {
+                    newSelectedRouteHolder = newHolder;
+                }
             }
         }
 
-        return createRouteFromAudioInfo(type);
-    }
-
-    @NonNull
-    private MediaRoute2Info createRouteFromAudioInfo(@MediaRoute2Info.Type int type) {
-        int name = R.string.default_audio_route_name;
-        switch (type) {
-            case TYPE_WIRED_HEADPHONES:
-            case TYPE_WIRED_HEADSET:
-                name = R.string.default_audio_route_name_headphones;
-                break;
-            case TYPE_DOCK:
-                name = R.string.default_audio_route_name_dock_speakers;
-                break;
-            case TYPE_HDMI:
-            case TYPE_HDMI_ARC:
-            case TYPE_HDMI_EARC:
-                name = R.string.default_audio_route_name_external_device;
-                break;
-            case TYPE_USB_DEVICE:
-                name = R.string.default_audio_route_name_usb;
-                break;
+        if (mRouteIdToAvailableDeviceRoutes.isEmpty()) {
+            // Due to an unknown reason (possibly an audio server crash), we ended up with an empty
+            // list of routes. Our entire codebase assumes at least one system route always exists,
+            // so we create a placeholder route represented as a built-in speaker for
+            // user-presentation purposes.
+            Slog.e(TAG, "Ended up with an empty list of routes. Creating a placeholder route.");
+            MediaRoute2InfoHolder placeholderRouteHolder = createPlaceholderBuiltinSpeakerRoute();
+            String placeholderRouteId = placeholderRouteHolder.mMediaRoute2Info.getId();
+            mRouteIdToAvailableDeviceRoutes.put(placeholderRouteId, placeholderRouteHolder);
         }
 
-        synchronized (this) {
-            return new MediaRoute2Info.Builder(
-                            MediaRoute2Info.ROUTE_ID_DEVICE,
-                            mContext.getResources().getText(name).toString())
-                    .setVolumeHandling(
-                            mAudioManager.isVolumeFixed()
-                                    ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED
-                                    : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
-                    .setVolume(mDeviceVolume)
-                    .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
-                    .setType(type)
-                    .addFeature(FEATURE_LIVE_AUDIO)
-                    .addFeature(FEATURE_LIVE_VIDEO)
-                    .addFeature(FEATURE_LOCAL_PLAYBACK)
-                    .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED)
-                    .build();
+        if (newSelectedRouteHolder == null) {
+            Slog.e(
+                    TAG,
+                    "Could not map this selected device attribute type to an available route: "
+                            + selectedDeviceAttributesType);
+            // We know mRouteIdToAvailableDeviceRoutes is not empty.
+            newSelectedRouteHolder = mRouteIdToAvailableDeviceRoutes.values().iterator().next();
+        }
+        MediaRoute2InfoHolder selectedRouteHolderWithUpdatedVolumeInfo =
+                newSelectedRouteHolder.copyWithVolumeInfoFromAudioManager(mAudioManager);
+        mRouteIdToAvailableDeviceRoutes.put(
+                newSelectedRouteHolder.mMediaRoute2Info.getId(),
+                selectedRouteHolderWithUpdatedVolumeInfo);
+        mSelectedRoute = selectedRouteHolderWithUpdatedVolumeInfo.mMediaRoute2Info;
+
+        // We only add those BT routes that we have not already obtained from audio manager (which
+        // are active).
+        mBluetoothRouteController.getAvailableBluetoothRoutes().stream()
+                .filter(it -> !mRouteIdToAvailableDeviceRoutes.containsKey(it.getId()))
+                .map(MediaRoute2InfoHolder::createForInactiveBluetoothRoute)
+                .forEach(
+                        it -> mRouteIdToAvailableDeviceRoutes.put(it.mMediaRoute2Info.getId(), it));
+    }
+
+    private MediaRoute2InfoHolder createPlaceholderBuiltinSpeakerRoute() {
+        int type = AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
+        return MediaRoute2InfoHolder.createForAudioManagerRoute(
+                createMediaRoute2Info(
+                        /* routeId= */ null, type, /* productName= */ null, /* address= */ null),
+                type);
+    }
+
+    @Nullable
+    private MediaRoute2Info createMediaRoute2InfoFromAudioDeviceInfo(
+            AudioDeviceInfo audioDeviceInfo) {
+        String address = audioDeviceInfo.getAddress();
+        // Passing a null route id means we want to get the default id for the route. Generally, we
+        // only expect to pass null for non-Bluetooth routes.
+        String routeId =
+                TextUtils.isEmpty(address)
+                        ? null
+                        : mBluetoothRouteController.getRouteIdForBluetoothAddress(address);
+        return createMediaRoute2Info(
+                routeId, audioDeviceInfo.getType(), audioDeviceInfo.getProductName(), address);
+    }
+
+    /**
+     * Creates a new {@link MediaRoute2Info} using the provided information.
+     *
+     * @param routeId A route id, or null to use an id pre-defined for the given {@code type}.
+     * @param audioDeviceInfoType The type as obtained from {@link AudioDeviceInfo#getType}.
+     * @param productName The product name as obtained from {@link
+     *     AudioDeviceInfo#getProductName()}, or null to use a predefined name for the given {@code
+     *     type}.
+     * @param address The type as obtained from {@link AudioDeviceInfo#getAddress()} or {@link
+     *     BluetoothDevice#getAddress()}.
+     * @return The new {@link MediaRoute2Info}.
+     */
+    @Nullable
+    private MediaRoute2Info createMediaRoute2Info(
+            @Nullable String routeId,
+            int audioDeviceInfoType,
+            @Nullable CharSequence productName,
+            @Nullable String address) {
+        SystemRouteInfo systemRouteInfo =
+                AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.get(audioDeviceInfoType);
+        if (systemRouteInfo == null) {
+            // Device type that's intentionally unsupported for media output, like the built-in
+            // earpiece.
+            return null;
+        }
+        CharSequence humanReadableName = productName;
+        if (TextUtils.isEmpty(humanReadableName)) {
+            humanReadableName = mContext.getResources().getText(systemRouteInfo.mNameResource);
+        }
+        if (routeId == null) {
+            // The caller hasn't provided an id, so we use a pre-defined one. This happens when we
+            // are creating a non-BT route, or we are creating a BT route but a race condition
+            // caused AudioManager to expose the BT route before BluetoothAdapter, preventing us
+            // from getting an id using BluetoothRouteController#getRouteIdForBluetoothAddress.
+            routeId = systemRouteInfo.mDefaultRouteId;
+        }
+        return new MediaRoute2Info.Builder(routeId, humanReadableName)
+                .setType(systemRouteInfo.mMediaRoute2InfoType)
+                .setAddress(address)
+                .setSystemRoute(true)
+                .addFeature(FEATURE_LIVE_AUDIO)
+                .addFeature(FEATURE_LOCAL_PLAYBACK)
+                .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED)
+                .build();
+    }
+
+    /**
+     * Holds a {@link MediaRoute2Info} and associated information that we don't want to put in the
+     * {@link MediaRoute2Info} class because it's solely necessary for the implementation of this
+     * class.
+     */
+    private static class MediaRoute2InfoHolder {
+
+        public final MediaRoute2Info mMediaRoute2Info;
+        public final int mAudioDeviceInfoType;
+        public final boolean mCorrespondsToInactiveBluetoothRoute;
+
+        public static MediaRoute2InfoHolder createForAudioManagerRoute(
+                MediaRoute2Info mediaRoute2Info, int audioDeviceInfoType) {
+            return new MediaRoute2InfoHolder(
+                    mediaRoute2Info,
+                    audioDeviceInfoType,
+                    /* correspondsToInactiveBluetoothRoute= */ false);
+        }
+
+        public static MediaRoute2InfoHolder createForInactiveBluetoothRoute(
+                MediaRoute2Info mediaRoute2Info) {
+            // There's no corresponding audio device info, hence the audio device info type is
+            // unknown.
+            return new MediaRoute2InfoHolder(
+                    mediaRoute2Info,
+                    /* audioDeviceInfoType= */ AudioDeviceInfo.TYPE_UNKNOWN,
+                    /* correspondsToInactiveBluetoothRoute= */ true);
+        }
+
+        private MediaRoute2InfoHolder(
+                MediaRoute2Info mediaRoute2Info,
+                int audioDeviceInfoType,
+                boolean correspondsToInactiveBluetoothRoute) {
+            mMediaRoute2Info = mediaRoute2Info;
+            mAudioDeviceInfoType = audioDeviceInfoType;
+            mCorrespondsToInactiveBluetoothRoute = correspondsToInactiveBluetoothRoute;
+        }
+
+        public MediaRoute2InfoHolder copyWithVolumeInfoFromAudioManager(
+                AudioManager mAudioManager) {
+            MediaRoute2Info routeInfoWithVolumeInfo =
+                    new MediaRoute2Info.Builder(mMediaRoute2Info)
+                            .setVolumeHandling(
+                                    mAudioManager.isVolumeFixed()
+                                            ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED
+                                            : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
+                            .setVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC))
+                            .setVolumeMax(
+                                    mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
+                            .build();
+            return new MediaRoute2InfoHolder(
+                    routeInfoWithVolumeInfo,
+                    mAudioDeviceInfoType,
+                    mCorrespondsToInactiveBluetoothRoute);
         }
     }
 
     /**
-     * Checks if the given type is a device route.
-     *
-     * <p>Device route means a route which is either built-in or wired to the current device.
-     *
-     * @param type specifies the type of the device.
-     * @return {@code true} if the device is wired or built-in and {@code false} otherwise.
+     * Holds route information about an {@link AudioDeviceInfo#getType() audio device info type}.
      */
-    private boolean isDeviceRouteType(@MediaRoute2Info.Type int type) {
-        switch (type) {
-            case TYPE_BUILTIN_SPEAKER:
-            case TYPE_WIRED_HEADPHONES:
-            case TYPE_WIRED_HEADSET:
-            case TYPE_DOCK:
-            case TYPE_HDMI:
-            case TYPE_HDMI_ARC:
-            case TYPE_HDMI_EARC:
-            case TYPE_USB_DEVICE:
-                return true;
-            default:
-                return false;
+    private static class SystemRouteInfo {
+        /** The type to use for {@link MediaRoute2Info#getType()}. */
+        public final int mMediaRoute2InfoType;
+
+        /**
+         * Holds the route id to use if no other id is provided.
+         *
+         * <p>We only expect this id to be used for non-bluetooth routes. For bluetooth routes, in a
+         * normal scenario, the id is generated from the device information (like address, or
+         * hiSyncId), and this value is ignored. A non-normal scenario may occur when there's race
+         * condition between {@link BluetoothAdapter} and {@link AudioManager}, who are not
+         * synchronized.
+         */
+        public final String mDefaultRouteId;
+
+        /**
+         * The name to use for {@link MediaRoute2Info#getName()}.
+         *
+         * <p>Usually replaced by the UI layer with a localized string.
+         */
+        public final int mNameResource;
+
+        private SystemRouteInfo(int mediaRoute2InfoType, String defaultRouteId, int nameResource) {
+            mMediaRoute2InfoType = mediaRoute2InfoType;
+            mDefaultRouteId = defaultRouteId;
+            mNameResource = nameResource;
         }
     }
 
-    private class AudioRoutesObserver extends IAudioRoutesObserver.Stub {
-
+    private class AudioDeviceCallbackImpl extends AudioDeviceCallback {
+        @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
         @Override
-        public void dispatchAudioRoutesChanged(AudioRoutesInfo newAudioRoutes) {
-            boolean isDeviceRouteChanged;
-            MediaRoute2Info deviceRoute = createRouteFromAudioInfo(newAudioRoutes);
-
-            synchronized (AudioPoliciesDeviceRouteController.this) {
-                mDeviceRoute = deviceRoute;
-                isDeviceRouteChanged = mSelectedRoute == null;
+        public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
+            for (AudioDeviceInfo deviceInfo : addedDevices) {
+                if (AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.contains(deviceInfo.getType())) {
+                    // When a new valid media output is connected, we clear any routing policies so
+                    // that the default routing logic from the audio framework kicks in. As a result
+                    // of this, when the user connects a bluetooth device or a wired headset, the
+                    // new device becomes the active route, which is the traditional behavior.
+                    mAudioManager.removePreferredDeviceForStrategy(mStrategyForMedia);
+                    rebuildAvailableRoutesAndNotify();
+                    break;
+                }
             }
+        }
 
-            if (isDeviceRouteChanged) {
-                mOnDeviceRouteChangedListener.onDeviceRouteChanged();
+        @RequiresPermission(
+                anyOf = {
+                    Manifest.permission.MODIFY_AUDIO_ROUTING,
+                    Manifest.permission.QUERY_AUDIO_STATE
+                })
+        @Override
+        public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
+            for (AudioDeviceInfo deviceInfo : removedDevices) {
+                if (AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.contains(deviceInfo.getType())) {
+                    rebuildAvailableRoutesAndNotify();
+                    break;
+                }
             }
         }
     }
 
+    static {
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_BUILTIN_SPEAKER,
+                        /* defaultRouteId= */ "ROUTE_ID_BUILTIN_SPEAKER",
+                        /* nameResource= */ R.string.default_audio_route_name));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_WIRED_HEADSET,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_WIRED_HEADSET,
+                        /* defaultRouteId= */ "ROUTE_ID_WIRED_HEADSET",
+                        /* nameResource= */ R.string.default_audio_route_name_headphones));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_WIRED_HEADPHONES,
+                        /* defaultRouteId= */ "ROUTE_ID_WIRED_HEADPHONES",
+                        /* nameResource= */ R.string.default_audio_route_name_headphones));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_BLUETOOTH_A2DP,
+                        /* defaultRouteId= */ "ROUTE_ID_BLUETOOTH_A2DP",
+                        /* nameResource= */ R.string.bluetooth_a2dp_audio_route_name));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_HDMI,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_HDMI,
+                        /* defaultRouteId= */ "ROUTE_ID_HDMI",
+                        /* nameResource= */ R.string.default_audio_route_name_external_device));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_DOCK,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_DOCK,
+                        /* defaultRouteId= */ "ROUTE_ID_DOCK",
+                        /* nameResource= */ R.string.default_audio_route_name_dock_speakers));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_USB_DEVICE,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_USB_DEVICE,
+                        /* defaultRouteId= */ "ROUTE_ID_USB_DEVICE",
+                        /* nameResource= */ R.string.default_audio_route_name_usb));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_USB_HEADSET,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_USB_HEADSET,
+                        /* defaultRouteId= */ "ROUTE_ID_USB_HEADSET",
+                        /* nameResource= */ R.string.default_audio_route_name_usb));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_HDMI_ARC,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_HDMI_ARC,
+                        /* defaultRouteId= */ "ROUTE_ID_HDMI_ARC",
+                        /* nameResource= */ R.string.default_audio_route_name_external_device));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_HDMI_EARC,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_HDMI_EARC,
+                        /* defaultRouteId= */ "ROUTE_ID_HDMI_EARC",
+                        /* nameResource= */ R.string.default_audio_route_name_external_device));
+        // TODO: b/305199571 - Add a proper type constants and human readable names for AUX_LINE,
+        // LINE_ANALOG, LINE_DIGITAL, BLE_BROADCAST, BLE_SPEAKER, BLE_HEADSET, and HEARING_AID.
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_HEARING_AID,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_HEARING_AID,
+                        /* defaultRouteId= */ "ROUTE_ID_HEARING_AID",
+                        /* nameResource= */ R.string.bluetooth_a2dp_audio_route_name));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_BLE_HEADSET,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_BLE_HEADSET,
+                        /* defaultRouteId= */ "ROUTE_ID_BLE_HEADSET",
+                        /* nameResource= */ R.string.bluetooth_a2dp_audio_route_name));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_BLE_SPEAKER,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_BLE_HEADSET, // TODO: b/305199571 - Make a new type.
+                        /* defaultRouteId= */ "ROUTE_ID_BLE_SPEAKER",
+                        /* nameResource= */ R.string.bluetooth_a2dp_audio_route_name));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_BLE_BROADCAST,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_BLE_HEADSET,
+                        /* defaultRouteId= */ "ROUTE_ID_BLE_BROADCAST",
+                        /* nameResource= */ R.string.bluetooth_a2dp_audio_route_name));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_LINE_DIGITAL,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_UNKNOWN,
+                        /* defaultRouteId= */ "ROUTE_ID_LINE_DIGITAL",
+                        /* nameResource= */ R.string.default_audio_route_name_external_device));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_LINE_ANALOG,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_UNKNOWN,
+                        /* defaultRouteId= */ "ROUTE_ID_LINE_ANALOG",
+                        /* nameResource= */ R.string.default_audio_route_name_external_device));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_AUX_LINE,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_UNKNOWN,
+                        /* defaultRouteId= */ "ROUTE_ID_AUX_LINE",
+                        /* nameResource= */ R.string.default_audio_route_name_external_device));
+        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
+                AudioDeviceInfo.TYPE_DOCK_ANALOG,
+                new SystemRouteInfo(
+                        MediaRoute2Info.TYPE_DOCK,
+                        /* defaultRouteId= */ "ROUTE_ID_DOCK_ANALOG",
+                        /* nameResource= */ R.string.default_audio_route_name_dock_speakers));
+    }
 }
diff --git a/services/core/java/com/android/server/media/AudioRoutingUtils.java b/services/core/java/com/android/server/media/AudioRoutingUtils.java
new file mode 100644
index 0000000..13f11eb
--- /dev/null
+++ b/services/core/java/com/android/server/media/AudioRoutingUtils.java
@@ -0,0 +1,46 @@
+/*
+ * 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.server.media;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.audiopolicy.AudioProductStrategy;
+
+/** Holds utils related to routing in the audio framework. */
+/* package */ final class AudioRoutingUtils {
+
+    /* package */ static final AudioAttributes ATTRIBUTES_MEDIA =
+            new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
+
+    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
+    @Nullable
+    /* package */ static AudioProductStrategy getMediaAudioProductStrategy() {
+        for (AudioProductStrategy strategy : AudioManager.getAudioProductStrategies()) {
+            if (strategy.supportsAudioAttributes(AudioRoutingUtils.ATTRIBUTES_MEDIA)) {
+                return strategy;
+            }
+        }
+        return null;
+    }
+
+    private AudioRoutingUtils() {
+        // no-op to prevent instantiation.
+    }
+}
diff --git a/services/core/java/com/android/server/media/BluetoothRouteController.java b/services/core/java/com/android/server/media/BluetoothRouteController.java
index 2b01001..74fdf6e 100644
--- a/services/core/java/com/android/server/media/BluetoothRouteController.java
+++ b/services/core/java/com/android/server/media/BluetoothRouteController.java
@@ -44,19 +44,11 @@
     @NonNull
     static BluetoothRouteController createInstance(@NonNull Context context,
             @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) {
-        Objects.requireNonNull(context);
         Objects.requireNonNull(listener);
+        BluetoothAdapter btAdapter = context.getSystemService(BluetoothManager.class).getAdapter();
 
-        BluetoothManager bluetoothManager = (BluetoothManager)
-                context.getSystemService(Context.BLUETOOTH_SERVICE);
-        BluetoothAdapter btAdapter = bluetoothManager.getAdapter();
-
-        if (btAdapter == null) {
+        if (btAdapter == null || Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
             return new NoOpBluetoothRouteController();
-        }
-
-        if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
-            return new AudioPoliciesBluetoothRouteController(context, btAdapter, listener);
         } else {
             return new LegacyBluetoothRouteController(context, btAdapter, listener);
         }
@@ -74,17 +66,6 @@
      */
     void stop();
 
-
-    /**
-     * Selects the route with the given {@code deviceAddress}.
-     *
-     * @param deviceAddress The physical address of the device to select. May be null to unselect
-     *                      the currently selected device.
-     * @return Whether the selection succeeds. If the selection fails, the state of the instance
-     * remains unaltered.
-     */
-    boolean selectRoute(@Nullable String deviceAddress);
-
     /**
      * Transfers Bluetooth output to the given route.
      *
@@ -158,12 +139,6 @@
         }
 
         @Override
-        public boolean selectRoute(String deviceAddress) {
-            // no op
-            return false;
-        }
-
-        @Override
         public void transferTo(String routeId) {
             // no op
         }
diff --git a/services/core/java/com/android/server/media/DeviceRouteController.java b/services/core/java/com/android/server/media/DeviceRouteController.java
index 0fdaaa7..9f175a9 100644
--- a/services/core/java/com/android/server/media/DeviceRouteController.java
+++ b/services/core/java/com/android/server/media/DeviceRouteController.java
@@ -16,17 +16,25 @@
 
 package com.android.server.media;
 
+import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
 import android.content.Context;
 import android.media.AudioManager;
-import android.media.IAudioRoutesObserver;
 import android.media.IAudioService;
 import android.media.MediaRoute2Info;
+import android.media.audiopolicy.AudioProductStrategy;
+import android.os.Looper;
 import android.os.ServiceManager;
+import android.os.UserHandle;
 
 import com.android.media.flags.Flags;
 
+import java.util.List;
+
 /**
  * Controls device routes.
  *
@@ -37,46 +45,67 @@
  */
 /* package */ interface DeviceRouteController {
 
-    /**
-     * Returns a new instance of {@link DeviceRouteController}.
-     */
-    /* package */ static DeviceRouteController createInstance(@NonNull Context context,
+    /** Returns a new instance of {@link DeviceRouteController}. */
+    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
+    /* package */ static DeviceRouteController createInstance(
+            @NonNull Context context,
+            @NonNull Looper looper,
             @NonNull OnDeviceRouteChangedListener onDeviceRouteChangedListener) {
         AudioManager audioManager = context.getSystemService(AudioManager.class);
-        IAudioService audioService = IAudioService.Stub.asInterface(
-                ServiceManager.getService(Context.AUDIO_SERVICE));
+        AudioProductStrategy strategyForMedia = AudioRoutingUtils.getMediaAudioProductStrategy();
 
-        if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
-            return new AudioPoliciesDeviceRouteController(context,
+        BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
+        BluetoothAdapter btAdapter =
+                bluetoothManager != null ? bluetoothManager.getAdapter() : null;
+
+        // TODO: b/305199571 - Make the audio policies implementation work without the need for a
+        // bluetooth adapter or a strategy for media. If no strategy for media is available we can
+        // disallow media router transfers, and without a bluetooth adapter we can remove support
+        // for transfers to inactive bluetooth routes.
+        if (strategyForMedia != null
+                && btAdapter != null
+                && Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
+            return new AudioPoliciesDeviceRouteController(
+                    context,
                     audioManager,
-                    audioService,
+                    looper,
+                    strategyForMedia,
+                    btAdapter,
                     onDeviceRouteChangedListener);
         } else {
-            return new LegacyDeviceRouteController(context,
-                    audioManager,
-                    audioService,
-                    onDeviceRouteChangedListener);
+            IAudioService audioService =
+                    IAudioService.Stub.asInterface(
+                            ServiceManager.getService(Context.AUDIO_SERVICE));
+            return new LegacyDeviceRouteController(
+                    context, audioManager, audioService, onDeviceRouteChangedListener);
         }
     }
 
-    /**
-     * Select the route with the given built-in or wired {@link MediaRoute2Info.Type}.
-     *
-     * <p>If the type is {@code null} then unselects the route and falls back to the default device
-     * route observed from
-     * {@link com.android.server.audio.AudioService#startWatchingRoutes(IAudioRoutesObserver)}.
-     *
-     * @param type device type. May be {@code null} to unselect currently selected route.
-     * @return whether the selection succeeds. If the selection fails the state of the controller
-     * remains intact.
-     */
-    boolean selectRoute(@Nullable @MediaRoute2Info.Type Integer type);
-
     /** Returns the currently selected device (built-in or wired) route. */
     @NonNull
     MediaRoute2Info getSelectedRoute();
 
     /**
+     * Returns all available routes.
+     *
+     * <p>Note that this method returns available routes including the selected route because (a)
+     * this interface doesn't guarantee that the internal state of the controller won't change
+     * between calls to {@link #getSelectedRoute()} and this method and (b) {@link
+     * #getSelectedRoute()} may be treated as a transferable route (not a selected route) if the
+     * selected route is from {@link BluetoothRouteController}.
+     */
+    List<MediaRoute2Info> getAvailableRoutes();
+
+    /**
+     * Transfers device output to the given route.
+     *
+     * <p>If the route is {@code null} then active route will be deactivated.
+     *
+     * @param routeId to switch to or {@code null} to unset the active device.
+     */
+    void transferTo(@Nullable String routeId);
+
+    /**
      * Updates device route volume.
      *
      * @param volume specifies a volume for the device route or 0 for unknown.
@@ -85,6 +114,18 @@
     boolean updateVolume(int volume);
 
     /**
+     * Starts listening for changes in the system to keep an up to date view of available and
+     * selected devices.
+     */
+    void start(UserHandle mUser);
+
+    /**
+     * Stops keeping the internal state up to date with the system, releasing any resources acquired
+     * in {@link #start}
+     */
+    void stop();
+
+    /**
      * Interface for receiving events when device route has changed.
      */
     interface OnDeviceRouteChangedListener {
diff --git a/services/core/java/com/android/server/media/LegacyBluetoothRouteController.java b/services/core/java/com/android/server/media/LegacyBluetoothRouteController.java
index ba3cecf..041fceaf 100644
--- a/services/core/java/com/android/server/media/LegacyBluetoothRouteController.java
+++ b/services/core/java/com/android/server/media/LegacyBluetoothRouteController.java
@@ -132,12 +132,6 @@
         mContext.unregisterReceiver(mDeviceStateChangedReceiver);
     }
 
-    @Override
-    public boolean selectRoute(String deviceAddress) {
-        // No-op as the class decides if a route is selected based on Bluetooth events.
-        return false;
-    }
-
     /**
      * Transfers to a given bluetooth route.
      * The dedicated BT device with the route would be activated.
diff --git a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
index 65874e2..c0f2834 100644
--- a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
+++ b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
@@ -35,11 +35,13 @@
 import android.media.IAudioService;
 import android.media.MediaRoute2Info;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.util.Slog;
 
 import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -73,7 +75,6 @@
     private int mDeviceVolume;
     private MediaRoute2Info mDeviceRoute;
 
-    @VisibleForTesting
     /* package */ LegacyDeviceRouteController(@NonNull Context context,
             @NonNull AudioManager audioManager,
             @NonNull IAudioService audioService,
@@ -100,9 +101,13 @@
     }
 
     @Override
-    public boolean selectRoute(@Nullable Integer type) {
-        // No-op as the controller does not support selection from the outside of the class.
-        return false;
+    public void start(UserHandle mUser) {
+        // Nothing to do.
+    }
+
+    @Override
+    public void stop() {
+        // Nothing to do.
     }
 
     @Override
@@ -112,6 +117,17 @@
     }
 
     @Override
+    public synchronized List<MediaRoute2Info> getAvailableRoutes() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public synchronized void transferTo(@Nullable String routeId) {
+        // Unsupported. This implementation doesn't support transferable routes (always exposes a
+        // single non-bluetooth route).
+    }
+
+    @Override
     public synchronized boolean updateVolume(int volume) {
         if (mDeviceVolume == volume) {
             return false;
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index c8dba80..86d7833 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -16,15 +16,12 @@
 
 package com.android.server.media;
 
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.media.AudioAttributes;
-import android.media.AudioDeviceAttributes;
 import android.media.AudioManager;
 import android.media.MediaRoute2Info;
 import android.media.MediaRoute2ProviderInfo;
@@ -51,7 +48,8 @@
  */
 // TODO: check thread safety. We may need to use lock to protect variables.
 class SystemMediaRoute2Provider extends MediaRoute2Provider {
-    private static final String TAG = "MR2SystemProvider";
+    // Package-visible to use this tag for all system routing logic (done across multiple classes).
+    /* package */ static final String TAG = "MR2SystemProvider";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final ComponentName COMPONENT_NAME = new ComponentName(
@@ -77,26 +75,6 @@
     private final AudioManagerBroadcastReceiver mAudioReceiver =
             new AudioManagerBroadcastReceiver();
 
-    private final AudioManager.OnDevicesForAttributesChangedListener
-            mOnDevicesForAttributesChangedListener =
-            new AudioManager.OnDevicesForAttributesChangedListener() {
-                @Override
-                public void onDevicesForAttributesChanged(@NonNull AudioAttributes attributes,
-                        @NonNull List<AudioDeviceAttributes> devices) {
-                    if (attributes.getUsage() != AudioAttributes.USAGE_MEDIA) {
-                        return;
-                    }
-
-                    mHandler.post(() -> {
-                        updateSelectedAudioDevice(devices);
-                        notifyProviderState();
-                        if (updateSessionInfosIfNeeded()) {
-                            notifySessionInfoUpdated();
-                        }
-                    });
-                }
-            };
-
     private final Object mRequestLock = new Object();
     @GuardedBy("mRequestLock")
     private volatile SessionCreationRequest mPendingSessionCreationRequest;
@@ -106,7 +84,8 @@
         mIsSystemRouteProvider = true;
         mContext = context;
         mUser = user;
-        mHandler = new Handler(Looper.getMainLooper());
+        Looper looper = Looper.getMainLooper();
+        mHandler = new Handler(looper);
 
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
 
@@ -123,25 +102,15 @@
         mDeviceRouteController =
                 DeviceRouteController.createInstance(
                         context,
-                        () -> {
-                            mHandler.post(
-                                    () -> {
-                                        publishProviderState();
-                                        if (updateSessionInfosIfNeeded()) {
-                                            notifySessionInfoUpdated();
-                                        }
-                                    });
-                        });
-
-        mAudioManager.addOnDevicesForAttributesChangedListener(
-                AudioAttributesUtils.ATTRIBUTES_MEDIA, mContext.getMainExecutor(),
-                mOnDevicesForAttributesChangedListener);
-
-        // These methods below should be called after all fields are initialized, as they
-        // access the fields inside.
-        List<AudioDeviceAttributes> devices =
-                mAudioManager.getDevicesForAttributes(AudioAttributesUtils.ATTRIBUTES_MEDIA);
-        updateSelectedAudioDevice(devices);
+                        looper,
+                        () ->
+                                mHandler.post(
+                                        () -> {
+                                            publishProviderState();
+                                            if (updateSessionInfosIfNeeded()) {
+                                                notifySessionInfoUpdated();
+                                            }
+                                        }));
         updateProviderState();
         updateSessionInfosIfNeeded();
     }
@@ -151,20 +120,21 @@
         intentFilter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
         mContext.registerReceiverAsUser(mAudioReceiver, mUser,
                 intentFilter, null, null);
-
-        mHandler.post(() -> {
-            mBluetoothRouteController.start(mUser);
-            notifyProviderState();
-        });
-        updateVolume();
+        mHandler.post(
+                () -> {
+                    mDeviceRouteController.start(mUser);
+                    mBluetoothRouteController.start(mUser);
+                });
     }
 
     public void stop() {
         mContext.unregisterReceiver(mAudioReceiver);
-        mHandler.post(() -> {
-            mBluetoothRouteController.stop();
-            notifyProviderState();
-        });
+        mHandler.post(
+                () -> {
+                    mBluetoothRouteController.stop();
+                    mDeviceRouteController.stop();
+                    notifyProviderState();
+                });
     }
 
     @Override
@@ -225,13 +195,26 @@
     public void transferToRoute(long requestId, String sessionId, String routeId) {
         if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
             // The currently selected route is the default route.
+            Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT);
             return;
         }
-
         MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute();
-        if (TextUtils.equals(routeId, selectedDeviceRoute.getId())) {
+        boolean isAvailableDeviceRoute =
+                mDeviceRouteController.getAvailableRoutes().stream()
+                        .anyMatch(it -> it.getId().equals(routeId));
+        boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRoute.getId());
+
+        if (isSelectedDeviceRoute || isAvailableDeviceRoute) {
+            // The requested route is managed by the device route controller. Note that the selected
+            // device route doesn't necessarily match mSelectedRouteId (which is the selected route
+            // of the routing session). If the selected device route is transferred to, we need to
+            // make the bluetooth routes inactive so that the device route becomes the selected
+            // route of the routing session.
+            mDeviceRouteController.transferTo(routeId);
             mBluetoothRouteController.transferTo(null);
         } else {
+            // The requested route is managed by the bluetooth route controller.
+            mDeviceRouteController.transferTo(null);
             mBluetoothRouteController.transferTo(routeId);
         }
     }
@@ -280,41 +263,38 @@
 
             MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute();
 
-            RoutingSessionInfo.Builder builder = new RoutingSessionInfo.Builder(
-                    SYSTEM_SESSION_ID, packageName).setSystemSession(true);
+            RoutingSessionInfo.Builder builder =
+                    new RoutingSessionInfo.Builder(SYSTEM_SESSION_ID, packageName)
+                            .setSystemSession(true);
             builder.addSelectedRoute(selectedDeviceRoute.getId());
             for (MediaRoute2Info route : mBluetoothRouteController.getAllBluetoothRoutes()) {
                 builder.addTransferableRoute(route.getId());
             }
+
+            if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
+                for (MediaRoute2Info route : mDeviceRouteController.getAvailableRoutes()) {
+                    if (!TextUtils.equals(selectedDeviceRoute.getId(), route.getId())) {
+                        builder.addTransferableRoute(route.getId());
+                    }
+                }
+            }
             return builder.setProviderId(mUniqueId).build();
         }
     }
 
-    private void updateSelectedAudioDevice(@NonNull List<AudioDeviceAttributes> devices) {
-        if (devices.isEmpty()) {
-            Slog.w(TAG, "The list of preferred devices was empty.");
-            return;
-        }
-
-        AudioDeviceAttributes audioDeviceAttributes = devices.get(0);
-
-        if (AudioAttributesUtils.isDeviceOutputAttributes(audioDeviceAttributes)) {
-            mDeviceRouteController.selectRoute(
-                    AudioAttributesUtils.mapToMediaRouteType(audioDeviceAttributes));
-            mBluetoothRouteController.selectRoute(null);
-        } else if (AudioAttributesUtils.isBluetoothOutputAttributes(audioDeviceAttributes)) {
-            mDeviceRouteController.selectRoute(null);
-            mBluetoothRouteController.selectRoute(audioDeviceAttributes.getAddress());
-        } else {
-            Slog.w(TAG, "Unknown audio attributes: " + audioDeviceAttributes);
-        }
-    }
-
     private void updateProviderState() {
         MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder();
 
         // We must have a device route in the provider info.
-        builder.addRoute(mDeviceRouteController.getSelectedRoute());
+        if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
+            List<MediaRoute2Info> deviceRoutes = mDeviceRouteController.getAvailableRoutes();
+            for (MediaRoute2Info route : deviceRoutes) {
+                builder.addRoute(route);
+            }
+            setProviderState(builder.build());
+        } else {
+            builder.addRoute(mDeviceRouteController.getSelectedRoute());
+        }
 
         for (MediaRoute2Info route : mBluetoothRouteController.getAllBluetoothRoutes()) {
             builder.addRoute(route);
@@ -352,7 +332,12 @@
                             .setProviderId(mUniqueId)
                             .build();
             builder.addSelectedRoute(mSelectedRouteId);
-
+            for (MediaRoute2Info route : mDeviceRouteController.getAvailableRoutes()) {
+                String routeId = route.getId();
+                if (!mSelectedRouteId.equals(routeId)) {
+                    builder.addTransferableRoute(routeId);
+                }
+            }
             for (MediaRoute2Info route : mBluetoothRouteController.getTransferableRoutes()) {
                 builder.addTransferableRoute(route.getId());
             }
diff --git a/services/robotests/src/com/android/server/media/AudioPoliciesBluetoothRouteControllerTest.java b/services/robotests/src/com/android/server/media/AudioPoliciesBluetoothRouteControllerTest.java
deleted file mode 100644
index 0ad4184..0000000
--- a/services/robotests/src/com/android/server/media/AudioPoliciesBluetoothRouteControllerTest.java
+++ /dev/null
@@ -1,293 +0,0 @@
-/*
- * 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.server.media;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.when;
-
-import android.app.Application;
-import android.bluetooth.BluetoothA2dp;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.BluetoothProfile;
-import android.content.Context;
-import android.content.Intent;
-import android.media.AudioManager;
-import android.media.MediaRoute2Info;
-import android.os.UserHandle;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.Shadows;
-import org.robolectric.shadows.ShadowBluetoothAdapter;
-import org.robolectric.shadows.ShadowBluetoothDevice;
-
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
-
-@RunWith(RobolectricTestRunner.class)
-public class AudioPoliciesBluetoothRouteControllerTest {
-
-    private static final String DEVICE_ADDRESS_UNKNOWN = ":unknown:ip:address:";
-    private static final String DEVICE_ADDRESS_SAMPLE_1 = "30:59:8B:E4:C6:35";
-    private static final String DEVICE_ADDRESS_SAMPLE_2 = "0D:0D:A6:FF:8D:B6";
-    private static final String DEVICE_ADDRESS_SAMPLE_3 = "2D:9B:0C:C2:6F:78";
-    private static final String DEVICE_ADDRESS_SAMPLE_4 = "66:88:F9:2D:A8:1E";
-
-    private Context mContext;
-
-    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
-
-    @Mock
-    private BluetoothRouteController.BluetoothRoutesUpdatedListener mListener;
-
-    @Mock
-    private BluetoothProfileMonitor mBluetoothProfileMonitor;
-
-    private AudioPoliciesBluetoothRouteController mAudioPoliciesBluetoothRouteController;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-
-        Application application = ApplicationProvider.getApplicationContext();
-        mContext = application;
-
-        BluetoothManager bluetoothManager = (BluetoothManager)
-                mContext.getSystemService(Context.BLUETOOTH_SERVICE);
-
-        BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
-        mShadowBluetoothAdapter = Shadows.shadowOf(bluetoothAdapter);
-
-        mAudioPoliciesBluetoothRouteController =
-                new AudioPoliciesBluetoothRouteController(mContext, bluetoothAdapter,
-                        mBluetoothProfileMonitor, mListener) {
-                    @Override
-                    boolean isDeviceConnected(BluetoothDevice device) {
-                        return true;
-                    }
-                };
-
-        // Enable A2DP profile.
-        when(mBluetoothProfileMonitor.isProfileSupported(eq(BluetoothProfile.A2DP), any()))
-                .thenReturn(true);
-        mShadowBluetoothAdapter.setProfileConnectionState(BluetoothProfile.A2DP,
-                BluetoothProfile.STATE_CONNECTED);
-
-        mAudioPoliciesBluetoothRouteController.start(UserHandle.of(0));
-    }
-
-    @Test
-    public void getSelectedRoute_noBluetoothRoutesAvailable_returnsNull() {
-        assertThat(mAudioPoliciesBluetoothRouteController.getSelectedRoute()).isNull();
-    }
-
-    @Test
-    public void selectRoute_noBluetoothRoutesAvailable_returnsFalse() {
-        assertThat(mAudioPoliciesBluetoothRouteController
-                .selectRoute(DEVICE_ADDRESS_UNKNOWN)).isFalse();
-    }
-
-    @Test
-    public void selectRoute_noDeviceWithGivenAddress_returnsFalse() {
-        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
-                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_3);
-
-        mShadowBluetoothAdapter.setBondedDevices(devices);
-
-        assertThat(mAudioPoliciesBluetoothRouteController
-                .selectRoute(DEVICE_ADDRESS_SAMPLE_2)).isFalse();
-    }
-
-    @Test
-    public void selectRoute_deviceIsInDevicesSet_returnsTrue() {
-        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
-                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_2);
-
-        mShadowBluetoothAdapter.setBondedDevices(devices);
-
-        assertThat(mAudioPoliciesBluetoothRouteController
-                .selectRoute(DEVICE_ADDRESS_SAMPLE_1)).isTrue();
-    }
-
-    @Test
-    public void selectRoute_resetSelectedDevice_returnsTrue() {
-        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
-                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_2);
-
-        mShadowBluetoothAdapter.setBondedDevices(devices);
-
-        mAudioPoliciesBluetoothRouteController.selectRoute(DEVICE_ADDRESS_SAMPLE_1);
-        assertThat(mAudioPoliciesBluetoothRouteController.selectRoute(null)).isTrue();
-    }
-
-    @Test
-    public void selectRoute_noSelectedDevice_returnsTrue() {
-        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
-                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_2);
-
-        mShadowBluetoothAdapter.setBondedDevices(devices);
-
-        assertThat(mAudioPoliciesBluetoothRouteController.selectRoute(null)).isTrue();
-    }
-
-    @Test
-    public void getSelectedRoute_updateRouteFailed_returnsNull() {
-        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
-                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_2);
-
-        mShadowBluetoothAdapter.setBondedDevices(devices);
-        mAudioPoliciesBluetoothRouteController
-                .selectRoute(DEVICE_ADDRESS_SAMPLE_3);
-
-        assertThat(mAudioPoliciesBluetoothRouteController.getSelectedRoute()).isNull();
-    }
-
-    @Test
-    public void getSelectedRoute_updateRouteSuccessful_returnsUpdateDevice() {
-        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
-                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_2, DEVICE_ADDRESS_SAMPLE_4);
-
-        assertThat(mAudioPoliciesBluetoothRouteController.getSelectedRoute()).isNull();
-
-        mShadowBluetoothAdapter.setBondedDevices(devices);
-
-        assertThat(mAudioPoliciesBluetoothRouteController
-                .selectRoute(DEVICE_ADDRESS_SAMPLE_4)).isTrue();
-
-        MediaRoute2Info selectedRoute = mAudioPoliciesBluetoothRouteController.getSelectedRoute();
-        assertThat(selectedRoute.getAddress()).isEqualTo(DEVICE_ADDRESS_SAMPLE_4);
-    }
-
-    @Test
-    public void getSelectedRoute_resetSelectedRoute_returnsNull() {
-        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
-                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_2, DEVICE_ADDRESS_SAMPLE_4);
-
-        mShadowBluetoothAdapter.setBondedDevices(devices);
-
-        // Device is not null now.
-        mAudioPoliciesBluetoothRouteController.selectRoute(DEVICE_ADDRESS_SAMPLE_4);
-        // Rest the device.
-        mAudioPoliciesBluetoothRouteController.selectRoute(null);
-
-        assertThat(mAudioPoliciesBluetoothRouteController.getSelectedRoute())
-                .isNull();
-    }
-
-    @Test
-    public void getTransferableRoutes_noSelectedRoute_returnsAllBluetoothDevices() {
-        String[] addresses = new String[] { DEVICE_ADDRESS_SAMPLE_1,
-                DEVICE_ADDRESS_SAMPLE_2, DEVICE_ADDRESS_SAMPLE_4 };
-        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(addresses);
-        mShadowBluetoothAdapter.setBondedDevices(devices);
-
-        // Force route controller to update bluetooth devices list.
-        sendBluetoothDevicesChangedBroadcast();
-
-        Set<String> transferableDevices = extractAddressesListFrom(
-                mAudioPoliciesBluetoothRouteController.getTransferableRoutes());
-        assertThat(transferableDevices).containsExactlyElementsIn(addresses);
-    }
-
-    @Test
-    public void getTransferableRoutes_hasSelectedRoute_returnsRoutesWithoutSelectedDevice() {
-        String[] addresses = new String[] { DEVICE_ADDRESS_SAMPLE_1,
-                DEVICE_ADDRESS_SAMPLE_2, DEVICE_ADDRESS_SAMPLE_4 };
-        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(addresses);
-        mShadowBluetoothAdapter.setBondedDevices(devices);
-
-        // Force route controller to update bluetooth devices list.
-        sendBluetoothDevicesChangedBroadcast();
-        mAudioPoliciesBluetoothRouteController.selectRoute(DEVICE_ADDRESS_SAMPLE_4);
-
-        Set<String> transferableDevices = extractAddressesListFrom(
-                mAudioPoliciesBluetoothRouteController.getTransferableRoutes());
-        assertThat(transferableDevices).containsExactly(DEVICE_ADDRESS_SAMPLE_1,
-                DEVICE_ADDRESS_SAMPLE_2);
-    }
-
-    @Test
-    public void getAllBluetoothRoutes_hasSelectedRoute_returnsAllRoutes() {
-        String[] addresses = new String[] { DEVICE_ADDRESS_SAMPLE_1,
-                DEVICE_ADDRESS_SAMPLE_2, DEVICE_ADDRESS_SAMPLE_4 };
-        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(addresses);
-        mShadowBluetoothAdapter.setBondedDevices(devices);
-
-        // Force route controller to update bluetooth devices list.
-        sendBluetoothDevicesChangedBroadcast();
-        mAudioPoliciesBluetoothRouteController.selectRoute(DEVICE_ADDRESS_SAMPLE_4);
-
-        Set<String> bluetoothDevices = extractAddressesListFrom(
-                mAudioPoliciesBluetoothRouteController.getAllBluetoothRoutes());
-        assertThat(bluetoothDevices).containsExactlyElementsIn(addresses);
-    }
-
-    @Test
-    public void updateVolumeForDevice_setVolumeForA2DPTo25_selectedRouteVolumeIsUpdated() {
-        String[] addresses = new String[] { DEVICE_ADDRESS_SAMPLE_1,
-                DEVICE_ADDRESS_SAMPLE_2, DEVICE_ADDRESS_SAMPLE_4 };
-        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(addresses);
-        mShadowBluetoothAdapter.setBondedDevices(devices);
-
-        // Force route controller to update bluetooth devices list.
-        sendBluetoothDevicesChangedBroadcast();
-        mAudioPoliciesBluetoothRouteController.selectRoute(DEVICE_ADDRESS_SAMPLE_4);
-
-        mAudioPoliciesBluetoothRouteController.updateVolumeForDevices(
-                AudioManager.DEVICE_OUT_BLUETOOTH_A2DP, 25);
-
-        MediaRoute2Info selectedRoute = mAudioPoliciesBluetoothRouteController.getSelectedRoute();
-        assertThat(selectedRoute.getVolume()).isEqualTo(25);
-    }
-
-    private void sendBluetoothDevicesChangedBroadcast() {
-        Intent intent = new Intent(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
-        mContext.sendBroadcast(intent);
-    }
-
-    private static Set<String> extractAddressesListFrom(Collection<MediaRoute2Info> routes) {
-        Set<String> addresses = new HashSet<>();
-
-        for (MediaRoute2Info route: routes) {
-            addresses.add(route.getAddress());
-        }
-
-        return addresses;
-    }
-
-    private static Set<BluetoothDevice> generateFakeBluetoothDevicesSet(String... addresses) {
-        Set<BluetoothDevice> devices = new HashSet<>();
-
-        for (String address: addresses) {
-            devices.add(ShadowBluetoothDevice.newInstance(address));
-        }
-
-        return devices;
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java b/services/tests/servicestests/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java
deleted file mode 100644
index 5aef7a3..0000000
--- a/services/tests/servicestests/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * 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.server.media;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.media.AudioManager;
-import android.media.AudioRoutesInfo;
-import android.media.IAudioRoutesObserver;
-import android.media.MediaRoute2Info;
-import android.os.RemoteException;
-
-import com.android.internal.R;
-import com.android.server.audio.AudioService;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(JUnit4.class)
-public class AudioPoliciesDeviceRouteControllerTest {
-
-    private static final String ROUTE_NAME_DEFAULT = "default";
-    private static final String ROUTE_NAME_DOCK = "dock";
-    private static final String ROUTE_NAME_HEADPHONES = "headphones";
-
-    private static final int VOLUME_SAMPLE_1 = 25;
-
-    @Mock
-    private Context mContext;
-    @Mock
-    private Resources mResources;
-    @Mock
-    private AudioManager mAudioManager;
-    @Mock
-    private AudioService mAudioService;
-    @Mock
-    private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
-
-    @Captor
-    private ArgumentCaptor<IAudioRoutesObserver.Stub> mAudioRoutesObserverCaptor;
-
-    private AudioPoliciesDeviceRouteController mController;
-
-    private IAudioRoutesObserver.Stub mAudioRoutesObserver;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-
-        when(mContext.getResources()).thenReturn(mResources);
-        when(mResources.getText(anyInt())).thenReturn(ROUTE_NAME_DEFAULT);
-
-        // Setting built-in speaker as default speaker.
-        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
-        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_SPEAKER;
-        when(mAudioService.startWatchingRoutes(mAudioRoutesObserverCaptor.capture()))
-                .thenReturn(audioRoutesInfo);
-
-        mController = new AudioPoliciesDeviceRouteController(
-                mContext, mAudioManager, mAudioService, mOnDeviceRouteChangedListener);
-
-        mAudioRoutesObserver = mAudioRoutesObserverCaptor.getValue();
-    }
-
-    @Test
-    public void getDeviceRoute_noSelectedRoutes_returnsDefaultDevice() {
-        MediaRoute2Info route2Info = mController.getSelectedRoute();
-
-        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DEFAULT);
-        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
-    }
-
-    @Test
-    public void getDeviceRoute_audioRouteHasChanged_returnsRouteFromAudioService() {
-        when(mResources.getText(R.string.default_audio_route_name_headphones))
-                .thenReturn(ROUTE_NAME_HEADPHONES);
-
-        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
-        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
-        callAudioRoutesObserver(audioRoutesInfo);
-
-        MediaRoute2Info route2Info = mController.getSelectedRoute();
-        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES);
-        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
-    }
-
-    @Test
-    public void getDeviceRoute_selectDevice_returnsSelectedRoute() {
-        when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
-                .thenReturn(ROUTE_NAME_DOCK);
-
-        mController.selectRoute(MediaRoute2Info.TYPE_DOCK);
-
-        MediaRoute2Info route2Info = mController.getSelectedRoute();
-        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DOCK);
-        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK);
-    }
-
-    @Test
-    public void getDeviceRoute_hasSelectedAndAudioServiceRoutes_returnsSelectedRoute() {
-        when(mResources.getText(R.string.default_audio_route_name_headphones))
-                .thenReturn(ROUTE_NAME_HEADPHONES);
-        when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
-                .thenReturn(ROUTE_NAME_DOCK);
-
-        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
-        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
-        callAudioRoutesObserver(audioRoutesInfo);
-
-        mController.selectRoute(MediaRoute2Info.TYPE_DOCK);
-
-        MediaRoute2Info route2Info = mController.getSelectedRoute();
-        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DOCK);
-        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK);
-    }
-
-    @Test
-    public void getDeviceRoute_unselectRoute_returnsAudioServiceRoute() {
-        when(mResources.getText(R.string.default_audio_route_name_headphones))
-                .thenReturn(ROUTE_NAME_HEADPHONES);
-        when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
-                .thenReturn(ROUTE_NAME_DOCK);
-
-        mController.selectRoute(MediaRoute2Info.TYPE_DOCK);
-
-        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
-        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
-        callAudioRoutesObserver(audioRoutesInfo);
-
-        mController.selectRoute(null);
-
-        MediaRoute2Info route2Info = mController.getSelectedRoute();
-        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES);
-        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
-    }
-
-    @Test
-    public void getDeviceRoute_selectRouteFails_returnsAudioServiceRoute() {
-        when(mResources.getText(R.string.default_audio_route_name_headphones))
-                .thenReturn(ROUTE_NAME_HEADPHONES);
-
-        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
-        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
-        callAudioRoutesObserver(audioRoutesInfo);
-
-        mController.selectRoute(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
-
-        MediaRoute2Info route2Info = mController.getSelectedRoute();
-        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES);
-        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
-    }
-
-    @Test
-    public void selectRoute_selectWiredRoute_returnsTrue() {
-        assertThat(mController.selectRoute(MediaRoute2Info.TYPE_HDMI)).isTrue();
-    }
-
-    @Test
-    public void selectRoute_selectBluetoothRoute_returnsFalse() {
-        assertThat(mController.selectRoute(MediaRoute2Info.TYPE_BLUETOOTH_A2DP)).isFalse();
-    }
-
-    @Test
-    public void selectRoute_unselectRoute_returnsTrue() {
-        assertThat(mController.selectRoute(null)).isTrue();
-    }
-
-    @Test
-    public void updateVolume_noSelectedRoute_deviceRouteVolumeChanged() {
-        when(mResources.getText(R.string.default_audio_route_name_headphones))
-                .thenReturn(ROUTE_NAME_HEADPHONES);
-
-        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
-        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
-        callAudioRoutesObserver(audioRoutesInfo);
-
-        mController.updateVolume(VOLUME_SAMPLE_1);
-
-        MediaRoute2Info route2Info = mController.getSelectedRoute();
-        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
-        assertThat(route2Info.getVolume()).isEqualTo(VOLUME_SAMPLE_1);
-    }
-
-    @Test
-    public void updateVolume_connectSelectedRouteLater_selectedRouteVolumeChanged() {
-        when(mResources.getText(R.string.default_audio_route_name_headphones))
-                .thenReturn(ROUTE_NAME_HEADPHONES);
-        when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
-                .thenReturn(ROUTE_NAME_DOCK);
-
-        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
-        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
-        callAudioRoutesObserver(audioRoutesInfo);
-
-        mController.updateVolume(VOLUME_SAMPLE_1);
-
-        mController.selectRoute(MediaRoute2Info.TYPE_DOCK);
-
-        MediaRoute2Info route2Info = mController.getSelectedRoute();
-        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK);
-        assertThat(route2Info.getVolume()).isEqualTo(VOLUME_SAMPLE_1);
-    }
-
-    /**
-     * Simulates {@link IAudioRoutesObserver.Stub#dispatchAudioRoutesChanged(AudioRoutesInfo)}
-     * from {@link AudioService}. This happens when there is a wired route change,
-     * like a wired headset being connected.
-     *
-     * @param audioRoutesInfo updated state of connected wired device
-     */
-    private void callAudioRoutesObserver(AudioRoutesInfo audioRoutesInfo) {
-        try {
-            // this is a captured observer implementation
-            // from WiredRoutesController's AudioService#startWatchingRoutes call
-            mAudioRoutesObserver.dispatchAudioRoutesChanged(audioRoutesInfo);
-        } catch (RemoteException exception) {
-            // Should not happen since the object is mocked.
-            assertWithMessage("An unexpected RemoteException happened.").fail();
-        }
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java b/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java
index 14b121d..0961b7d 100644
--- a/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java
@@ -19,6 +19,7 @@
 import static com.android.media.flags.Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER;
 
 import android.content.Context;
+import android.os.Looper;
 import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
@@ -56,7 +57,8 @@
     @RequiresFlagsDisabled(FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER)
     public void createInstance_audioPoliciesFlagIsDisabled_createsLegacyController() {
         DeviceRouteController deviceRouteController =
-                DeviceRouteController.createInstance(mContext, mOnDeviceRouteChangedListener);
+                DeviceRouteController.createInstance(
+                        mContext, Looper.getMainLooper(), mOnDeviceRouteChangedListener);
 
         Truth.assertThat(deviceRouteController).isInstanceOf(LegacyDeviceRouteController.class);
     }
@@ -65,7 +67,8 @@
     @RequiresFlagsEnabled(FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER)
     public void createInstance_audioPoliciesFlagIsEnabled_createsAudioPoliciesController() {
         DeviceRouteController deviceRouteController =
-                DeviceRouteController.createInstance(mContext, mOnDeviceRouteChangedListener);
+                DeviceRouteController.createInstance(
+                        mContext, Looper.getMainLooper(), mOnDeviceRouteChangedListener);
 
         Truth.assertThat(deviceRouteController)
                 .isInstanceOf(AudioPoliciesDeviceRouteController.class);