Merge "Revert "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
new file mode 100644
index 0000000..8cb334d
--- /dev/null
+++ b/services/core/java/com/android/server/media/AudioAttributesUtils.java
@@ -0,0 +1,125 @@
+/*
+ * 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 a00999d..8bc69c2 100644
--- a/services/core/java/com/android/server/media/AudioPoliciesBluetoothRouteController.java
+++ b/services/core/java/com/android/server/media/AudioPoliciesBluetoothRouteController.java
@@ -17,6 +17,7 @@
 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;
@@ -30,37 +31,38 @@
 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;
 
 /**
- * Maintains a list of connected {@link BluetoothDevice bluetooth devices} and allows their
- * activation.
+ * Controls bluetooth routes and provides selected route override.
  *
- * <p>This class also serves as ground truth for assigning {@link MediaRoute2Info#getId() route ids}
- * for bluetooth routes via {@link #getRouteIdForBluetoothAddress}.
+ * <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.
  */
-// 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;
+/* package */ class AudioPoliciesBluetoothRouteController
+        implements BluetoothRouteController {
+    private static final String TAG = "APBtRouteController";
 
     private static final String HEARING_AID_ROUTE_ID_PREFIX = "HEARING_AID_";
     private static final String LE_AUDIO_ROUTE_ID_PREFIX = "LE_AUDIO_";
@@ -73,8 +75,11 @@
     private final DeviceStateChangedReceiver mDeviceStateChangedReceiver =
             new DeviceStateChangedReceiver();
 
-    @NonNull private Map<String, BluetoothDevice> mAddressToBondedDevice = new HashMap<>();
-    @NonNull private final Map<String, BluetoothRouteInfo> mBluetoothRoutes = new HashMap<>();
+    @NonNull
+    private final Map<String, BluetoothRouteInfo> mBluetoothRoutes = new HashMap<>();
+
+    @NonNull
+    private final SparseIntArray mVolumeMap = new SparseIntArray();
 
     @NonNull
     private final Context mContext;
@@ -84,6 +89,11 @@
     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,
@@ -97,12 +107,21 @@
             @NonNull BluetoothAdapter bluetoothAdapter,
             @NonNull BluetoothProfileMonitor bluetoothProfileMonitor,
             @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) {
-        mContext = Objects.requireNonNull(context);
-        mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter);
-        mBluetoothProfileMonitor = Objects.requireNonNull(bluetoothProfileMonitor);
-        mListener = Objects.requireNonNull(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();
     }
 
+    @Override
     public void start(UserHandle user) {
         mBluetoothProfileMonitor.start();
 
@@ -114,63 +133,122 @@
 
         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);
     }
 
-    @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;
+    @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;
+        }
     }
 
-    public synchronized void activateBluetoothDeviceWithAddress(String address) {
-        BluetoothRouteInfo btRouteInfo = mBluetoothRoutes.get(address);
+    /**
+     * 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);
+                }
+            }
+        }
+    }
 
-        if (btRouteInfo == null) {
-            Slog.w(TAG, "activateBluetoothDeviceWithAddress: Ignoring unknown address " + address);
+    @Override
+    public void transferTo(@Nullable String routeId) {
+        if (routeId == null) {
+            mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_AUDIO);
             return;
         }
+
+        BluetoothRouteInfo btRouteInfo = findBluetoothRouteWithRouteId(routeId);
+
+        if (btRouteInfo == null) {
+            Slog.w(TAG, "transferTo: Unknown route. ID=" + routeId);
+            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();
-            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()));
+
+            // We need to query all available to BT stack devices in order to avoid inconsistency
+            // between external services, like, AndroidManager, and BT stack.
             for (BluetoothDevice device : bondedDevices) {
-                if (device.isConnected()) {
+                if (isDeviceConnected(device)) {
                     BluetoothRouteInfo newBtRoute = createBluetoothRoute(device);
                     if (newBtRoute.mConnectedProfiles.size() > 0) {
                         mBluetoothRoutes.put(device.getAddress(), newBtRoute);
@@ -180,51 +258,106 @@
         }
     }
 
-    @NonNull
-    public List<MediaRoute2Info> getAvailableBluetoothRoutes() {
-        List<MediaRoute2Info> routes = new ArrayList<>();
-        Set<String> routeIds = new HashSet<>();
+    @VisibleForTesting
+        /* package */ boolean isDeviceConnected(@NonNull BluetoothDevice device) {
+        return device.isConnected();
+    }
 
+    @Nullable
+    @Override
+    public MediaRoute2Info getSelectedRoute() {
         synchronized (this) {
-            for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) {
-                // See createBluetoothRoute for info on why we do this.
-                if (routeIds.add(btRoute.mRoute.getId())) {
-                    routes.add(btRoute.mRoute);
-                }
+            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() {
+        List<MediaRoute2Info> routes = new ArrayList<>();
+        List<String> routeIds = new ArrayList<>();
+
+        MediaRoute2Info selectedRoute = getSelectedRoute();
+        if (selectedRoute != null) {
+            routes.add(selectedRoute);
+            routeIds.add(selectedRoute.getId());
+        }
+
+        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;
+                }
+                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)) {
@@ -232,6 +365,7 @@
         }
         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;
@@ -243,27 +377,66 @@
             type = MediaRoute2Info.TYPE_BLE_HEADSET;
         }
 
-        // 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();
+        // 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();
         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 {
@@ -295,6 +468,9 @@
         @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 246d68d..6bdfae2 100644
--- a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java
+++ b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java
@@ -17,596 +17,228 @@
 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.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.os.RemoteException;
 import android.util.Slog;
-import android.util.SparseArray;
 
 import com.android.internal.R;
-import com.android.server.media.BluetoothRouteController.NoOpBluetoothRouteController;
+import com.android.internal.annotations.VisibleForTesting;
 
-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 = SystemMediaRoute2Provider.TAG;
+
+    private static final String TAG = "APDeviceRoutesController";
 
     @NonNull
-    private static final AudioAttributes MEDIA_USAGE_AUDIO_ATTRIBUTES =
-            new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
+    private final Context mContext;
+    @NonNull
+    private final AudioManager mAudioManager;
+    @NonNull
+    private final IAudioService mAudioService;
 
     @NonNull
-    private static final SparseArray<SystemRouteInfo> AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO =
-            new SparseArray<>();
+    private final OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
+    @NonNull
+    private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver();
 
-    @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;
+    private int mDeviceVolume;
 
     @NonNull
-    private final Map<String, MediaRoute2InfoHolder> mRouteIdToAvailableDeviceRoutes =
-            new HashMap<>();
+    private MediaRoute2Info mDeviceRoute;
+    @Nullable
+    private MediaRoute2Info mSelectedRoute;
 
-    @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,
+    @VisibleForTesting
+    /* package */ AudioPoliciesDeviceRouteController(@NonNull Context context,
             @NonNull AudioManager audioManager,
-            @NonNull Looper looper,
-            @NonNull AudioProductStrategy strategyForMedia,
-            @NonNull BluetoothAdapter btAdapter,
+            @NonNull IAudioService audioService,
             @NonNull OnDeviceRouteChangedListener onDeviceRouteChangedListener) {
-        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();
+        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);
     }
 
-    @RequiresPermission(
-            anyOf = {
-                Manifest.permission.MODIFY_AUDIO_ROUTING,
-                Manifest.permission.QUERY_AUDIO_STATE
-            })
     @Override
-    public void start(UserHandle mUser) {
-        mBluetoothRouteController.start(mUser);
-        mAudioManager.registerAudioDeviceCallback(mAudioDeviceCallback, mHandler);
-        mAudioManager.addOnDevicesForAttributesChangedListener(
-                AudioRoutingUtils.ATTRIBUTES_MEDIA,
-                new HandlerExecutor(mHandler),
-                mOnDevicesForAttributesChangedListener);
-    }
+    public synchronized boolean selectRoute(@Nullable Integer type) {
+        if (type == null) {
+            mSelectedRoute = null;
+            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);
+        if (!isDeviceRouteType(type)) {
+            return false;
+        }
+
+        mSelectedRoute = createRouteFromAudioInfo(type);
+        return true;
     }
 
     @Override
     @NonNull
     public synchronized MediaRoute2Info getSelectedRoute() {
-        return mSelectedRoute;
+        if (mSelectedRoute != null) {
+            return mSelectedRoute;
+        }
+        return mDeviceRoute;
     }
 
     @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) {
-        // 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();
+        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();
+
         return true;
     }
 
-    @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();
-        }
-    }
+    @NonNull
+    private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) {
+        int type = TYPE_BUILTIN_SPEAKER;
 
-    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;
-                }
+        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;
             }
         }
 
-        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);
-        }
-
-        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));
+        return createRouteFromAudioInfo(type);
     }
 
-    private MediaRoute2InfoHolder createPlaceholderBuiltinSpeakerRoute() {
-        int type = AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
-        return MediaRoute2InfoHolder.createForAudioManagerRoute(
-                createMediaRoute2Info(
-                        /* routeId= */ null, type, /* productName= */ null, /* address= */ null),
-                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;
+        }
 
-    @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);
+        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();
+        }
     }
 
     /**
-     * Creates a new {@link MediaRoute2Info} using the provided information.
+     * Checks if the given type is a device route.
      *
-     * @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}.
+     * <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.
      */
-    @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);
+    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;
         }
     }
 
-    /**
-     * Holds route information about an {@link AudioDeviceInfo#getType() audio device info type}.
-     */
-    private static class SystemRouteInfo {
-        /** The type to use for {@link MediaRoute2Info#getType()}. */
-        public final int mMediaRoute2InfoType;
+    private class AudioRoutesObserver extends IAudioRoutesObserver.Stub {
 
-        /**
-         * 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 AudioDeviceCallbackImpl extends AudioDeviceCallback {
-        @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
         @Override
-        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;
-                }
+        public void dispatchAudioRoutesChanged(AudioRoutesInfo newAudioRoutes) {
+            boolean isDeviceRouteChanged;
+            MediaRoute2Info deviceRoute = createRouteFromAudioInfo(newAudioRoutes);
+
+            synchronized (AudioPoliciesDeviceRouteController.this) {
+                mDeviceRoute = deviceRoute;
+                isDeviceRouteChanged = mSelectedRoute == null;
             }
-        }
 
-        @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;
-                }
+            if (isDeviceRouteChanged) {
+                mOnDeviceRouteChangedListener.onDeviceRouteChanged();
             }
         }
     }
 
-    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
deleted file mode 100644
index 13f11eb..0000000
--- a/services/core/java/com/android/server/media/AudioRoutingUtils.java
+++ /dev/null
@@ -1,46 +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.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 74fdf6e..2b01001 100644
--- a/services/core/java/com/android/server/media/BluetoothRouteController.java
+++ b/services/core/java/com/android/server/media/BluetoothRouteController.java
@@ -44,11 +44,19 @@
     @NonNull
     static BluetoothRouteController createInstance(@NonNull Context context,
             @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) {
+        Objects.requireNonNull(context);
         Objects.requireNonNull(listener);
-        BluetoothAdapter btAdapter = context.getSystemService(BluetoothManager.class).getAdapter();
 
-        if (btAdapter == null || Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
+        BluetoothManager bluetoothManager = (BluetoothManager)
+                context.getSystemService(Context.BLUETOOTH_SERVICE);
+        BluetoothAdapter btAdapter = bluetoothManager.getAdapter();
+
+        if (btAdapter == null) {
             return new NoOpBluetoothRouteController();
+        }
+
+        if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
+            return new AudioPoliciesBluetoothRouteController(context, btAdapter, listener);
         } else {
             return new LegacyBluetoothRouteController(context, btAdapter, listener);
         }
@@ -66,6 +74,17 @@
      */
     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.
      *
@@ -139,6 +158,12 @@
         }
 
         @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 9f175a9..0fdaaa7 100644
--- a/services/core/java/com/android/server/media/DeviceRouteController.java
+++ b/services/core/java/com/android/server/media/DeviceRouteController.java
@@ -16,25 +16,17 @@
 
 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.
  *
@@ -45,67 +37,46 @@
  */
 /* package */ interface DeviceRouteController {
 
-    /** Returns a new instance of {@link DeviceRouteController}. */
-    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
-    /* package */ static DeviceRouteController createInstance(
-            @NonNull Context context,
-            @NonNull Looper looper,
+    /**
+     * Returns a new instance of {@link DeviceRouteController}.
+     */
+    /* package */ static DeviceRouteController createInstance(@NonNull Context context,
             @NonNull OnDeviceRouteChangedListener onDeviceRouteChangedListener) {
         AudioManager audioManager = context.getSystemService(AudioManager.class);
-        AudioProductStrategy strategyForMedia = AudioRoutingUtils.getMediaAudioProductStrategy();
+        IAudioService audioService = IAudioService.Stub.asInterface(
+                ServiceManager.getService(Context.AUDIO_SERVICE));
 
-        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,
+        if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
+            return new AudioPoliciesDeviceRouteController(context,
                     audioManager,
-                    looper,
-                    strategyForMedia,
-                    btAdapter,
+                    audioService,
                     onDeviceRouteChangedListener);
         } else {
-            IAudioService audioService =
-                    IAudioService.Stub.asInterface(
-                            ServiceManager.getService(Context.AUDIO_SERVICE));
-            return new LegacyDeviceRouteController(
-                    context, audioManager, audioService, onDeviceRouteChangedListener);
+            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.
@@ -114,18 +85,6 @@
     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 041fceaf..ba3cecf 100644
--- a/services/core/java/com/android/server/media/LegacyBluetoothRouteController.java
+++ b/services/core/java/com/android/server/media/LegacyBluetoothRouteController.java
@@ -132,6 +132,12 @@
         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 c0f2834..65874e2 100644
--- a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
+++ b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
@@ -35,13 +35,11 @@
 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;
 
 /**
@@ -75,6 +73,7 @@
     private int mDeviceVolume;
     private MediaRoute2Info mDeviceRoute;
 
+    @VisibleForTesting
     /* package */ LegacyDeviceRouteController(@NonNull Context context,
             @NonNull AudioManager audioManager,
             @NonNull IAudioService audioService,
@@ -101,13 +100,9 @@
     }
 
     @Override
-    public void start(UserHandle mUser) {
-        // Nothing to do.
-    }
-
-    @Override
-    public void stop() {
-        // Nothing to do.
+    public boolean selectRoute(@Nullable Integer type) {
+        // No-op as the controller does not support selection from the outside of the class.
+        return false;
     }
 
     @Override
@@ -117,17 +112,6 @@
     }
 
     @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 86d7833..c8dba80 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -16,12 +16,15 @@
 
 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;
@@ -48,8 +51,7 @@
  */
 // TODO: check thread safety. We may need to use lock to protect variables.
 class SystemMediaRoute2Provider extends MediaRoute2Provider {
-    // Package-visible to use this tag for all system routing logic (done across multiple classes).
-    /* package */ static final String TAG = "MR2SystemProvider";
+    private static final String TAG = "MR2SystemProvider";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final ComponentName COMPONENT_NAME = new ComponentName(
@@ -75,6 +77,26 @@
     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;
@@ -84,8 +106,7 @@
         mIsSystemRouteProvider = true;
         mContext = context;
         mUser = user;
-        Looper looper = Looper.getMainLooper();
-        mHandler = new Handler(looper);
+        mHandler = new Handler(Looper.getMainLooper());
 
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
 
@@ -102,15 +123,25 @@
         mDeviceRouteController =
                 DeviceRouteController.createInstance(
                         context,
-                        looper,
-                        () ->
-                                mHandler.post(
-                                        () -> {
-                                            publishProviderState();
-                                            if (updateSessionInfosIfNeeded()) {
-                                                notifySessionInfoUpdated();
-                                            }
-                                        }));
+                        () -> {
+                            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);
         updateProviderState();
         updateSessionInfosIfNeeded();
     }
@@ -120,21 +151,20 @@
         intentFilter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
         mContext.registerReceiverAsUser(mAudioReceiver, mUser,
                 intentFilter, null, null);
-        mHandler.post(
-                () -> {
-                    mDeviceRouteController.start(mUser);
-                    mBluetoothRouteController.start(mUser);
-                });
+
+        mHandler.post(() -> {
+            mBluetoothRouteController.start(mUser);
+            notifyProviderState();
+        });
+        updateVolume();
     }
 
     public void stop() {
         mContext.unregisterReceiver(mAudioReceiver);
-        mHandler.post(
-                () -> {
-                    mBluetoothRouteController.stop();
-                    mDeviceRouteController.stop();
-                    notifyProviderState();
-                });
+        mHandler.post(() -> {
+            mBluetoothRouteController.stop();
+            notifyProviderState();
+        });
     }
 
     @Override
@@ -195,26 +225,13 @@
     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();
-        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);
+        MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute();
+        if (TextUtils.equals(routeId, selectedDeviceRoute.getId())) {
             mBluetoothRouteController.transferTo(null);
         } else {
-            // The requested route is managed by the bluetooth route controller.
-            mDeviceRouteController.transferTo(null);
             mBluetoothRouteController.transferTo(routeId);
         }
     }
@@ -263,38 +280,41 @@
 
             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.
-        if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
-            List<MediaRoute2Info> deviceRoutes = mDeviceRouteController.getAvailableRoutes();
-            for (MediaRoute2Info route : deviceRoutes) {
-                builder.addRoute(route);
-            }
-            setProviderState(builder.build());
-        } else {
-            builder.addRoute(mDeviceRouteController.getSelectedRoute());
-        }
+        builder.addRoute(mDeviceRouteController.getSelectedRoute());
 
         for (MediaRoute2Info route : mBluetoothRouteController.getAllBluetoothRoutes()) {
             builder.addRoute(route);
@@ -332,12 +352,7 @@
                             .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
new file mode 100644
index 0000000..0ad4184
--- /dev/null
+++ b/services/robotests/src/com/android/server/media/AudioPoliciesBluetoothRouteControllerTest.java
@@ -0,0 +1,293 @@
+/*
+ * 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
new file mode 100644
index 0000000..5aef7a3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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 0961b7d..14b121d 100644
--- a/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java
@@ -19,7 +19,6 @@
 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;
@@ -57,8 +56,7 @@
     @RequiresFlagsDisabled(FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER)
     public void createInstance_audioPoliciesFlagIsDisabled_createsLegacyController() {
         DeviceRouteController deviceRouteController =
-                DeviceRouteController.createInstance(
-                        mContext, Looper.getMainLooper(), mOnDeviceRouteChangedListener);
+                DeviceRouteController.createInstance(mContext, mOnDeviceRouteChangedListener);
 
         Truth.assertThat(deviceRouteController).isInstanceOf(LegacyDeviceRouteController.class);
     }
@@ -67,8 +65,7 @@
     @RequiresFlagsEnabled(FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER)
     public void createInstance_audioPoliciesFlagIsEnabled_createsAudioPoliciesController() {
         DeviceRouteController deviceRouteController =
-                DeviceRouteController.createInstance(
-                        mContext, Looper.getMainLooper(), mOnDeviceRouteChangedListener);
+                DeviceRouteController.createInstance(mContext, mOnDeviceRouteChangedListener);
 
         Truth.assertThat(deviceRouteController)
                 .isInstanceOf(AudioPoliciesDeviceRouteController.class);