diff options
author | 2023-11-29 17:37:15 +0000 | |
---|---|---|
committer | 2023-11-29 17:37:15 +0000 | |
commit | 8bfb6e2ba05d53ccba1d207eac8eec8ab7a9d2ec (patch) | |
tree | 6c70ad2a89bf0751eae0b27f79aeb715e300cdd7 | |
parent | 20ccece6958d44e17470e26a971cdc0d99615680 (diff) | |
parent | 2aa6c9aec42ecfda9c9fd3746cda79952496e1ca (diff) |
Merge "Revert "Add support for wired routing"" into main
12 files changed, 1225 insertions, 806 deletions
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 000000000000..8cb334dc2260 --- /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 a00999d08b5b..8bc69c226d1a 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.BroadcastReceiver; 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 @@ import java.util.stream.Collectors; 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 @@ import java.util.stream.Collectors; 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 @@ import java.util.stream.Collectors; @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 @@ import java.util.stream.Collectors; 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); + } + } + } + } + + @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, "activateBluetoothDeviceWithAddress: Ignoring unknown address " + address); + 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 @@ import java.util.stream.Collectors; } } + @VisibleForTesting + /* package */ boolean isDeviceConnected(@NonNull BluetoothDevice device) { + return device.isConnected(); + } + + @Nullable + @Override + public MediaRoute2Info getSelectedRoute() { + synchronized (this) { + if (mSelectedBluetoothRoute == null) { + return null; + } + + return mSelectedBluetoothRoute.mRoute; + } + } + @NonNull - public List<MediaRoute2Info> getAvailableBluetoothRoutes() { + @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<>(); - Set<String> routeIds = new HashSet<>(); + 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()) { - // See createBluetoothRoute for info on why we do this. - if (routeIds.add(btRoute.mRoute.getId())) { - routes.add(btRoute.mRoute); + // 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 @@ import java.util.stream.Collectors; } 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 @@ import java.util.stream.Collectors; 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 @@ import java.util.stream.Collectors; @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 27df00f30531..6bdfae2dc02f 100644 --- a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java +++ b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java @@ -17,590 +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; - @NonNull - private static final AudioAttributes MEDIA_USAGE_AUDIO_ATTRIBUTES = - new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(); + private static final String TAG = "APDeviceRoutesController"; @NonNull - private static final SparseArray<SystemRouteInfo> AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO = - new SparseArray<>(); - - @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 final Context mContext; + @NonNull + private final AudioManager mAudioManager; @NonNull - private final Map<String, MediaRoute2InfoHolder> mRouteIdToAvailableDeviceRoutes = - new HashMap<>(); + private final IAudioService mAudioService; - @NonNull private final AudioProductStrategy mStrategyForMedia; + @NonNull + private final OnDeviceRouteChangedListener mOnDeviceRouteChangedListener; + @NonNull + private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver(); - @NonNull private final AudioDeviceCallback mAudioDeviceCallback = new AudioDeviceCallbackImpl(); + private int mDeviceVolume; @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, + private MediaRoute2Info mDeviceRoute; + @Nullable + private MediaRoute2Info mSelectedRoute; + + @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::rebuildAvailableRoutes); - rebuildAvailableRoutes(); - } + Objects.requireNonNull(context); + Objects.requireNonNull(audioManager); + Objects.requireNonNull(audioService); + Objects.requireNonNull(onDeviceRouteChangedListener); - @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); - } + mContext = context; + mOnDeviceRouteChangedListener = onDeviceRouteChangedListener; - @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); - } + mAudioManager = audioManager; + mAudioService = audioService; - @Override - @NonNull - public synchronized MediaRoute2Info getSelectedRoute() { - return mSelectedRoute; - } + 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); + } - @Override - @NonNull - public synchronized List<MediaRoute2Info> getAvailableRoutes() { - return mRouteIdToAvailableDeviceRoutes.values().stream() - .map(it -> it.mMediaRoute2Info) - .toList(); + mDeviceRoute = createRouteFromAudioInfo(newAudioRoutes); } - @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; + public synchronized boolean selectRoute(@Nullable Integer type) { + if (type == null) { + mSelectedRoute = null; + return true; } - 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); + + if (!isDeviceRouteType(type)) { + return false; } - } - @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. - rebuildAvailableRoutes(); + mSelectedRoute = createRouteFromAudioInfo(type); 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. - rebuildAvailableRoutes(); + @Override + @NonNull + public synchronized MediaRoute2Info getSelectedRoute() { + if (mSelectedRoute != null) { + return mSelectedRoute; } + return mDeviceRoute; } - @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; - } - } + @Override + public synchronized boolean updateVolume(int volume) { + if (mDeviceVolume == volume) { + return false; } - 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); - } + mDeviceVolume = volume; - 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(); + if (mSelectedRoute != null) { + mSelectedRoute = new MediaRoute2Info.Builder(mSelectedRoute) + .setVolume(volume) + .build(); } - 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)); - mOnDeviceRouteChangedListener.onDeviceRouteChanged(); - } - - private MediaRoute2InfoHolder createPlaceholderBuiltinSpeakerRoute() { - int type = AudioDeviceInfo.TYPE_BUILTIN_SPEAKER; - return MediaRoute2InfoHolder.createForAudioManagerRoute( - createMediaRoute2Info( - /* routeId= */ null, type, /* productName= */ null, /* address= */ null), - type); - } - - @Nullable - private MediaRoute2Info createMediaRoute2InfoFromAudioDeviceInfo( - AudioDeviceInfo audioDeviceInfo) { - String address = audioDeviceInfo.getAddress(); - // Passing a null route id means we want to get the default id for the route. Generally, we - // only expect to pass null for non-Bluetooth routes. - String routeId = - TextUtils.isEmpty(address) - ? null - : mBluetoothRouteController.getRouteIdForBluetoothAddress(address); - return createMediaRoute2Info( - routeId, audioDeviceInfo.getType(), audioDeviceInfo.getProductName(), address); - } - /** - * Creates a new {@link MediaRoute2Info} using the provided information. - * - * @param routeId A route id, or null to use an id pre-defined for the given {@code type}. - * @param audioDeviceInfoType The type as obtained from {@link AudioDeviceInfo#getType}. - * @param productName The product name as obtained from {@link - * AudioDeviceInfo#getProductName()}, or null to use a predefined name for the given {@code - * type}. - * @param address The type as obtained from {@link AudioDeviceInfo#getAddress()} or {@link - * BluetoothDevice#getAddress()}. - * @return The new {@link MediaRoute2Info}. - */ - @Nullable - private MediaRoute2Info createMediaRoute2Info( - @Nullable String routeId, - int audioDeviceInfoType, - @Nullable CharSequence productName, - @Nullable String address) { - SystemRouteInfo systemRouteInfo = - AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.get(audioDeviceInfoType); - if (systemRouteInfo == null) { - // Device type that's intentionally unsupported for media output, like the built-in - // earpiece. - return null; - } - CharSequence humanReadableName = productName; - if (TextUtils.isEmpty(humanReadableName)) { - humanReadableName = mContext.getResources().getText(systemRouteInfo.mNameResource); - } - if (routeId == null) { - // The caller hasn't provided an id, so we use a pre-defined one. This happens when we - // are creating a non-BT route, or we are creating a BT route but a race condition - // caused AudioManager to expose the BT route before BluetoothAdapter, preventing us - // from getting an id using BluetoothRouteController#getRouteIdForBluetoothAddress. - routeId = systemRouteInfo.mDefaultRouteId; - } - return new MediaRoute2Info.Builder(routeId, humanReadableName) - .setType(systemRouteInfo.mMediaRoute2InfoType) - .setAddress(address) - .setSystemRoute(true) - .addFeature(FEATURE_LIVE_AUDIO) - .addFeature(FEATURE_LOCAL_PLAYBACK) - .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED) + mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute) + .setVolume(volume) .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); - } + return true; + } - 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); + @NonNull + private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) { + int type = TYPE_BUILTIN_SPEAKER; + + if (newRoutes != null) { + if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) { + type = TYPE_WIRED_HEADPHONES; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { + type = TYPE_WIRED_HEADSET; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { + type = TYPE_DOCK; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) { + type = TYPE_HDMI; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) { + type = TYPE_USB_DEVICE; + } } - private MediaRoute2InfoHolder( - MediaRoute2Info mediaRoute2Info, - int audioDeviceInfoType, - boolean correspondsToInactiveBluetoothRoute) { - mMediaRoute2Info = mediaRoute2Info; - mAudioDeviceInfoType = audioDeviceInfoType; - mCorrespondsToInactiveBluetoothRoute = correspondsToInactiveBluetoothRoute; - } + return createRouteFromAudioInfo(type); + } - 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); + @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; + } + + 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(); } } /** - * Holds route information about an {@link AudioDeviceInfo#getType() audio device info type}. + * Checks if the given type is a device route. + * + * <p>Device route means a route which is either built-in or wired to the current device. + * + * @param type specifies the type of the device. + * @return {@code true} if the device is wired or built-in and {@code false} otherwise. */ - private static class SystemRouteInfo { - /** The type to use for {@link MediaRoute2Info#getType()}. */ - public final int mMediaRoute2InfoType; - - /** - * Holds the route id to use if no other id is provided. - * - * <p>We only expect this id to be used for non-bluetooth routes. For bluetooth routes, in a - * normal scenario, the id is generated from the device information (like address, or - * hiSyncId), and this value is ignored. A non-normal scenario may occur when there's race - * condition between {@link BluetoothAdapter} and {@link AudioManager}, who are not - * synchronized. - */ - public final String mDefaultRouteId; - - /** - * The name to use for {@link MediaRoute2Info#getName()}. - * - * <p>Usually replaced by the UI layer with a localized string. - */ - public final int mNameResource; - - private SystemRouteInfo(int mediaRoute2InfoType, String defaultRouteId, int nameResource) { - mMediaRoute2InfoType = mediaRoute2InfoType; - mDefaultRouteId = defaultRouteId; - mNameResource = nameResource; + private boolean isDeviceRouteType(@MediaRoute2Info.Type int type) { + switch (type) { + case TYPE_BUILTIN_SPEAKER: + case TYPE_WIRED_HEADPHONES: + case TYPE_WIRED_HEADSET: + case TYPE_DOCK: + case TYPE_HDMI: + case TYPE_HDMI_ARC: + case TYPE_HDMI_EARC: + case TYPE_USB_DEVICE: + return true; + default: + return false; } } - private class AudioDeviceCallbackImpl extends AudioDeviceCallback { - @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING) + private class AudioRoutesObserver extends IAudioRoutesObserver.Stub { + @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); - rebuildAvailableRoutes(); - 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())) { - rebuildAvailableRoutes(); - 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 13f11eb80ece..000000000000 --- 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 74fdf6ee1d7f..2b01001fd7d1 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 @@ import java.util.Objects; @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 @@ import java.util.Objects; */ 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 @@ import java.util.Objects; } @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 9f175a9a0277..0fdaaa7604e5 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,65 +37,44 @@ import java.util.List; */ /* 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); } } - /** 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. + * Select the route with the given built-in or wired {@link MediaRoute2Info.Type}. * - * <p>If the route is {@code null} then active route will be deactivated. + * <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 routeId to switch to or {@code null} to unset the active device. + * @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. */ - void transferTo(@Nullable String routeId); + boolean selectRoute(@Nullable @MediaRoute2Info.Type Integer type); + + /** Returns the currently selected device (built-in or wired) route. */ + @NonNull + MediaRoute2Info getSelectedRoute(); /** * Updates device route volume. @@ -114,18 +85,6 @@ import java.util.List; 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 041fceaf8d3d..ba3cecf7c091 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 @@ class LegacyBluetoothRouteController implements BluetoothRouteController { 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 c0f28346705c..65874e23dcdc 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.IAudioRoutesObserver; 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 @@ import java.util.Objects; private int mDeviceVolume; private MediaRoute2Info mDeviceRoute; + @VisibleForTesting /* package */ LegacyDeviceRouteController(@NonNull Context context, @NonNull AudioManager audioManager, @NonNull IAudioService audioService, @@ -101,13 +100,9 @@ import java.util.Objects; } @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 @@ import java.util.Objects; } @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 86d78334d546..c8dba800a017 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 @@ import java.util.Set; */ // 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 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { 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 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { 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 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { 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 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { 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 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { 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); + 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 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { 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 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { .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 000000000000..0ad418427183 --- /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 000000000000..5aef7a320930 --- /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 0961b7d97177..14b121d3945c 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 @@ package com.android.server.media; 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 @@ public class DeviceRouteControllerTest { @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 @@ public class DeviceRouteControllerTest { @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); |