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);