From c0184adb64974c8a1f947a923832909ecec465db Mon Sep 17 00:00:00 2001 From: Eric Laurent Date: Fri, 7 Apr 2023 19:27:03 +0200 Subject: [PATCH 1/2] AudioService: synchronize BT_SCO on and A2DP or LE suspended states Handle A2DP and LE Audio suspend requests in AudioService and AudioDeviceBroker to enforce permissions and keep a single synchronized ground truth for the audio HAL state. Make sure that A2DP and LE Audio are suspended before BT_SCO is turned ON and unsuspended if needed if BT_SCO is turned OFF. Bug: 265077417 Test: make Change-Id: Iebf877d80084cb9ac67bb21e96379ef24721ecc1 --- media/java/android/media/AudioManager.java | 14 +- media/java/android/media/IAudioService.aidl | 6 + .../server/audio/AudioDeviceBroker.java | 160 +++++++++++++++++- .../server/audio/AudioDeviceInventory.java | 10 +- .../android/server/audio/AudioService.java | 20 +++ .../com/android/server/audio/BtHelper.java | 4 +- 6 files changed, 197 insertions(+), 17 deletions(-) diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index b1d2e33df3f79..4759689335e96 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -3730,7 +3730,12 @@ public class AudioManager { @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @RequiresPermission(Manifest.permission.BLUETOOTH_STACK) public void setA2dpSuspended(boolean enable) { - AudioSystem.setParameters("A2dpSuspended=" + enable); + final IAudioService service = getService(); + try { + service.setA2dpSuspended(enable); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -3743,7 +3748,12 @@ public class AudioManager { @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @RequiresPermission(Manifest.permission.BLUETOOTH_STACK) public void setLeAudioSuspended(boolean enable) { - AudioSystem.setParameters("LeAudioSuspended=" + enable); + final IAudioService service = getService(); + try { + service.setLeAudioSuspended(enable); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index fe5afc5a717e6..7ce189ba85d58 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -231,6 +231,12 @@ interface IAudioService { void setBluetoothScoOn(boolean on); + @EnforcePermission("BLUETOOTH_STACK") + void setA2dpSuspended(boolean on); + + @EnforcePermission("BLUETOOTH_STACK") + void setLeAudioSuspended(boolean enable); + boolean isBluetoothScoOn(); void setBluetoothA2dpOn(boolean on); diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 12681565f84c2..2da9fa6088db5 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -204,6 +204,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private void init() { setupMessaging(mContext); + initAudioHalBluetoothState(); initRoutingStrategyIds(); mPreferredCommunicationDevice = null; updateActiveCommunicationDevice(); @@ -845,10 +846,100 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - /** - * Current Bluetooth SCO audio active state indicated by BtHelper via setBluetoothScoOn(). - */ + // Current Bluetooth SCO audio active state indicated by BtHelper via setBluetoothScoOn(). + @GuardedBy("mDeviceStateLock") private boolean mBluetoothScoOn; + // value of BT_SCO parameter currently applied to audio HAL. + @GuardedBy("mDeviceStateLock") + private boolean mBluetoothScoOnApplied; + + // A2DP suspend state requested by AudioManager.setA2dpSuspended() API. + @GuardedBy("mDeviceStateLock") + private boolean mBluetoothA2dpSuspendedExt; + // A2DP suspend state requested by AudioDeviceInventory. + @GuardedBy("mDeviceStateLock") + private boolean mBluetoothA2dpSuspendedInt; + // value of BT_A2dpSuspendedSCO parameter currently applied to audio HAL. + @GuardedBy("mDeviceStateLock") + private boolean mBluetoothA2dpSuspendedApplied; + + // LE Audio suspend state requested by AudioManager.setLeAudioSuspended() API. + @GuardedBy("mDeviceStateLock") + private boolean mBluetoothLeSuspendedExt; + // LE Audio suspend state requested by AudioDeviceInventory. + @GuardedBy("mDeviceStateLock") + private boolean mBluetoothLeSuspendedInt; + // value of LeAudioSuspended parameter currently applied to audio HAL. + @GuardedBy("mDeviceStateLock") + private boolean mBluetoothLeSuspendedApplied; + + private void initAudioHalBluetoothState() { + mBluetoothScoOnApplied = false; + AudioSystem.setParameters("BT_SCO=off"); + mBluetoothA2dpSuspendedApplied = false; + AudioSystem.setParameters("A2dpSuspended=false"); + mBluetoothLeSuspendedApplied = false; + AudioSystem.setParameters("LeAudioSuspended=false"); + } + + @GuardedBy("mDeviceStateLock") + private void updateAudioHalBluetoothState() { + if (mBluetoothScoOn != mBluetoothScoOnApplied) { + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "updateAudioHalBluetoothState() mBluetoothScoOn: " + + mBluetoothScoOn + ", mBluetoothScoOnApplied: " + mBluetoothScoOnApplied); + } + if (mBluetoothScoOn) { + if (!mBluetoothA2dpSuspendedApplied) { + AudioSystem.setParameters("A2dpSuspended=true"); + mBluetoothA2dpSuspendedApplied = true; + } + if (!mBluetoothLeSuspendedApplied) { + AudioSystem.setParameters("LeAudioSuspended=true"); + mBluetoothLeSuspendedApplied = true; + } + AudioSystem.setParameters("BT_SCO=on"); + } else { + AudioSystem.setParameters("BT_SCO=off"); + } + mBluetoothScoOnApplied = mBluetoothScoOn; + } + if (!mBluetoothScoOnApplied) { + if ((mBluetoothA2dpSuspendedExt || mBluetoothA2dpSuspendedInt) + != mBluetoothA2dpSuspendedApplied) { + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "updateAudioHalBluetoothState() mBluetoothA2dpSuspendedExt: " + + mBluetoothA2dpSuspendedExt + + ", mBluetoothA2dpSuspendedInt: " + mBluetoothA2dpSuspendedInt + + ", mBluetoothA2dpSuspendedApplied: " + + mBluetoothA2dpSuspendedApplied); + } + mBluetoothA2dpSuspendedApplied = + mBluetoothA2dpSuspendedExt || mBluetoothA2dpSuspendedInt; + if (mBluetoothA2dpSuspendedApplied) { + AudioSystem.setParameters("A2dpSuspended=true"); + } else { + AudioSystem.setParameters("A2dpSuspended=false"); + } + } + if ((mBluetoothLeSuspendedExt || mBluetoothLeSuspendedInt) + != mBluetoothLeSuspendedApplied) { + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "updateAudioHalBluetoothState() mBluetoothLeSuspendedExt: " + + mBluetoothLeSuspendedExt + + ", mBluetoothLeSuspendedInt: " + mBluetoothLeSuspendedInt + + ", mBluetoothLeSuspendedApplied: " + mBluetoothLeSuspendedApplied); + } + mBluetoothLeSuspendedApplied = + mBluetoothLeSuspendedExt || mBluetoothLeSuspendedInt; + if (mBluetoothLeSuspendedApplied) { + AudioSystem.setParameters("LeAudioSuspended=true"); + } else { + AudioSystem.setParameters("LeAudioSuspended=false"); + } + } + } + } /*package*/ void setBluetoothScoOn(boolean on, String eventSource) { if (AudioService.DEBUG_COMM_RTE) { @@ -856,10 +947,67 @@ import java.util.concurrent.atomic.AtomicBoolean; } synchronized (mDeviceStateLock) { mBluetoothScoOn = on; + updateAudioHalBluetoothState(); postUpdateCommunicationRouteClient(eventSource); } } + /*package*/ void setA2dpSuspended(boolean enable, boolean internal, String eventSource) { + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "setA2dpSuspended source: " + eventSource + ", enable: " + + enable + ", internal: " + internal + + ", mBluetoothA2dpSuspendedInt: " + mBluetoothA2dpSuspendedInt + + ", mBluetoothA2dpSuspendedExt: " + mBluetoothA2dpSuspendedExt); + } + synchronized (mDeviceStateLock) { + if (internal) { + mBluetoothA2dpSuspendedInt = enable; + } else { + mBluetoothA2dpSuspendedExt = enable; + } + updateAudioHalBluetoothState(); + } + } + + /*package*/ void clearA2dpSuspended() { + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "clearA2dpSuspended"); + } + synchronized (mDeviceStateLock) { + mBluetoothA2dpSuspendedInt = false; + mBluetoothA2dpSuspendedExt = false; + updateAudioHalBluetoothState(); + } + } + + /*package*/ void setLeAudioSuspended(boolean enable, boolean internal, String eventSource) { + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "setLeAudioSuspended source: " + eventSource + ", enable: " + + enable + ", internal: " + internal + + ", mBluetoothLeSuspendedInt: " + mBluetoothA2dpSuspendedInt + + ", mBluetoothLeSuspendedExt: " + mBluetoothA2dpSuspendedExt); + } + synchronized (mDeviceStateLock) { + if (internal) { + mBluetoothLeSuspendedInt = enable; + } else { + mBluetoothLeSuspendedExt = enable; + } + updateAudioHalBluetoothState(); + } + } + + /*package*/ void clearLeAudioSuspended() { + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "clearLeAudioSuspended"); + } + synchronized (mDeviceStateLock) { + mBluetoothLeSuspendedInt = false; + mBluetoothLeSuspendedExt = false; + updateAudioHalBluetoothState(); + } + } + /*package*/ AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) { synchronized (mDeviceStateLock) { return mDeviceInventory.startWatchingRoutes(observer); @@ -1992,12 +2140,6 @@ import java.util.concurrent.atomic.AtomicBoolean; "updateCommunicationRoute, preferredCommunicationDevice: " + preferredCommunicationDevice + " eventSource: " + eventSource))); - if (preferredCommunicationDevice == null - || preferredCommunicationDevice.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_SCO) { - AudioSystem.setParameters("BT_SCO=off"); - } else { - AudioSystem.setParameters("BT_SCO=on"); - } if (preferredCommunicationDevice == null) { AudioDeviceAttributes defaultDevice = getDefaultCommunicationDevice(); if (defaultDevice != null) { diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 43063afb4ce7a..228bc87cc20b9 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -1167,7 +1167,7 @@ public class AudioDeviceInventory { } // Reset A2DP suspend state each time a new sink is connected - mAudioSystem.setParameters("A2dpSuspended=false"); + mDeviceBroker.clearA2dpSuspended(); // The convention for head tracking sensors associated with A2DP devices is to // use a UUID derived from the MAC address as follows: @@ -1237,7 +1237,8 @@ public class AudioDeviceInventory { private void makeA2dpDeviceUnavailableLater(String address, int delayMs) { // prevent any activity on the A2DP audio output to avoid unwanted // reconnection of the sink. - mAudioSystem.setParameters("A2dpSuspended=true"); + mDeviceBroker.setA2dpSuspended( + true /*enable*/, true /*internal*/, "makeA2dpDeviceUnavailableLater"); // retrieve DeviceInfo before removing device final String deviceKey = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); @@ -1360,7 +1361,7 @@ public class AudioDeviceInventory { } // Reset LEA suspend state each time a new sink is connected - mAudioSystem.setParameters("LeAudioSuspended=false"); + mDeviceBroker.clearLeAudioSuspended(); mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address), new DeviceInfo(device, name, address, AudioSystem.AUDIO_FORMAT_DEFAULT)); @@ -1409,7 +1410,8 @@ public class AudioDeviceInventory { private void makeLeAudioDeviceUnavailableLater(String address, int device, int delayMs) { // prevent any activity on the LEA output to avoid unwanted // reconnection of the sink. - mAudioSystem.setParameters("LeAudioSuspended=true"); + mDeviceBroker.setLeAudioSuspended( + true /*enable*/, true /*internal*/, "makeLeAudioDeviceUnavailableLater"); // the device will be made unavailable later, so consider it disconnected right away mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address)); // send the delayed message to make the device unavailable later diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index ac55f28a8ab00..263ddf44a3c17 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -6376,6 +6376,26 @@ public class AudioService extends IAudioService.Stub mDeviceBroker.setBluetoothScoOn(on, eventSource); } + /** @see AudioManager#setA2dpSuspended(boolean) */ + @android.annotation.EnforcePermission(android.Manifest.permission.BLUETOOTH_STACK) + public void setA2dpSuspended(boolean enable) { + super.setA2dpSuspended_enforcePermission(); + final String eventSource = new StringBuilder("setA2dpSuspended(").append(enable) + .append(") from u/pid:").append(Binder.getCallingUid()).append("/") + .append(Binder.getCallingPid()).toString(); + mDeviceBroker.setA2dpSuspended(enable, false /*internal*/, eventSource); + } + + /** @see AudioManager#setA2dpSuspended(boolean) */ + @android.annotation.EnforcePermission(android.Manifest.permission.BLUETOOTH_STACK) + public void setLeAudioSuspended(boolean enable) { + super.setLeAudioSuspended_enforcePermission(); + final String eventSource = new StringBuilder("setLeAudioSuspended(").append(enable) + .append(") from u/pid:").append(Binder.getCallingUid()).append("/") + .append(Binder.getCallingPid()).toString(); + mDeviceBroker.setLeAudioSuspended(enable, false /*internal*/, eventSource); + } + /** @see AudioManager#isBluetoothScoOn() * Note that it doesn't report internal state, but state seen by apps (which may have * called setBluetoothScoOn() */ diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 631d7f5a170de..8c27c3ecfd872 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -492,8 +492,8 @@ public class BtHelper { /*package*/ synchronized void resetBluetoothSco() { mScoAudioState = SCO_STATE_INACTIVE; broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); - AudioSystem.setParameters("A2dpSuspended=false"); - AudioSystem.setParameters("LeAudioSuspended=false"); + mDeviceBroker.clearA2dpSuspended(); + mDeviceBroker.clearLeAudioSuspended(); mDeviceBroker.setBluetoothScoOn(false, "resetBluetoothSco"); } From 453f4ddef03a670b7365849ab86f10e299232371 Mon Sep 17 00:00:00 2001 From: Eric Laurent Date: Fri, 16 Dec 2022 15:12:50 +0100 Subject: [PATCH 2/2] AudioService: BT dual mode support Add support for mode selection per audio profile for connected Bluetooth devices. The modes are retrieved from BluetoothDevice.getPreferredAudioProfiles() and used to enable or disable a particular device for use by routing strategies associated to full duplex or playback only operations. The BT Stack will ensure that the supported modes/profiles for a given dual mode device are consistent (for instance LE Audio and A2DP cannot be enabled at the same time for OUTPUT_ONLY mode). Bug: 265077417 Test: connect dual mode Bluetooth headset and switch profiles Change-Id: I8d360d47092f13a0fd44a8534dab4dfa54027ef7 --- media/java/android/media/AudioSystem.java | 63 ++ .../server/audio/AudioDeviceBroker.java | 74 +- .../server/audio/AudioDeviceInventory.java | 819 +++++++++++++++--- .../server/audio/AudioSystemAdapter.java | 15 +- .../com/android/server/audio/BtHelper.java | 93 +- 5 files changed, 838 insertions(+), 226 deletions(-) diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index e73cf87ba9f3d..3123ee6dd4d71 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -1237,6 +1237,9 @@ public class AudioSystem public static final Set DEVICE_IN_ALL_SCO_SET; /** @hide */ public static final Set DEVICE_IN_ALL_USB_SET; + /** @hide */ + public static final Set DEVICE_IN_ALL_BLE_SET; + static { DEVICE_IN_ALL_SET = new HashSet<>(); DEVICE_IN_ALL_SET.add(DEVICE_IN_COMMUNICATION); @@ -1276,6 +1279,66 @@ public class AudioSystem DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_ACCESSORY); DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_DEVICE); DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_HEADSET); + + DEVICE_IN_ALL_BLE_SET = new HashSet<>(); + DEVICE_IN_ALL_BLE_SET.add(DEVICE_IN_BLE_HEADSET); + } + + /** @hide */ + public static boolean isBluetoothDevice(int deviceType) { + return isBluetoothA2dpOutDevice(deviceType) + || isBluetoothScoDevice(deviceType) + || isBluetoothLeDevice(deviceType); + } + + /** @hide */ + public static boolean isBluetoothOutDevice(int deviceType) { + return isBluetoothA2dpOutDevice(deviceType) + || isBluetoothScoOutDevice(deviceType) + || isBluetoothLeOutDevice(deviceType); + } + + /** @hide */ + public static boolean isBluetoothInDevice(int deviceType) { + return isBluetoothScoInDevice(deviceType) + || isBluetoothLeInDevice(deviceType); + } + + /** @hide */ + public static boolean isBluetoothA2dpOutDevice(int deviceType) { + return DEVICE_OUT_ALL_A2DP_SET.contains(deviceType); + } + + /** @hide */ + public static boolean isBluetoothScoOutDevice(int deviceType) { + return DEVICE_OUT_ALL_SCO_SET.contains(deviceType); + } + + /** @hide */ + public static boolean isBluetoothScoInDevice(int deviceType) { + return DEVICE_IN_ALL_SCO_SET.contains(deviceType); + } + + /** @hide */ + public static boolean isBluetoothScoDevice(int deviceType) { + return isBluetoothScoOutDevice(deviceType) + || isBluetoothScoInDevice(deviceType); + } + + /** @hide */ + public static boolean isBluetoothLeOutDevice(int deviceType) { + return DEVICE_OUT_ALL_BLE_SET.contains(deviceType); + } + + /** @hide */ + public static boolean isBluetoothLeInDevice(int deviceType) { + return DEVICE_IN_ALL_BLE_SET.contains(deviceType); + } + + /** @hide */ + public static boolean isBluetoothLeDevice(int deviceType) { + return isBluetoothLeOutDevice(deviceType) + || isBluetoothLeInDevice(deviceType); } /** @hide */ diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 2da9fa6088db5..462942e4cb276 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -87,14 +87,14 @@ import java.util.concurrent.atomic.AtomicBoolean; private final @NonNull Context mContext; /** ID for Communication strategy retrieved form audio policy manager */ - private int mCommunicationStrategyId = -1; + /*package*/ int mCommunicationStrategyId = -1; /** ID for Accessibility strategy retrieved form audio policy manager */ private int mAccessibilityStrategyId = -1; /** Active communication device reported by audio policy manager */ - private AudioDeviceInfo mActiveCommunicationDevice; + /*package*/ AudioDeviceInfo mActiveCommunicationDevice; /** Last preferred device set for communication strategy */ private AudioDeviceAttributes mPreferredCommunicationDevice; @@ -750,6 +750,19 @@ import java.util.concurrent.atomic.AtomicBoolean; mIsLeOutput = false; } + BtDeviceInfo(@NonNull BtDeviceInfo src, int state) { + mDevice = src.mDevice; + mState = state; + mProfile = src.mProfile; + mSupprNoisy = src.mSupprNoisy; + mVolume = src.mVolume; + mIsLeOutput = src.mIsLeOutput; + mEventSource = src.mEventSource; + mAudioSystemDevice = src.mAudioSystemDevice; + mMusicDevice = src.mMusicDevice; + mCodec = src.mCodec; + } + // redefine equality op so we can match messages intended for this device @Override public boolean equals(Object o) { @@ -816,7 +829,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * @param info struct with the (dis)connection information */ /*package*/ void queueOnBluetoothActiveDeviceChanged(@NonNull BtDeviceChangedData data) { - if (data.mInfo.getProfile() == BluetoothProfile.A2DP && data.mPreviousDevice != null + if (data.mPreviousDevice != null && data.mPreviousDevice.equals(data.mNewDevice)) { final String name = TextUtils.emptyIfNull(data.mNewDevice.getName()); new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE + MediaMetrics.SEPARATOR @@ -825,7 +838,8 @@ import java.util.concurrent.atomic.AtomicBoolean; .set(MediaMetrics.Property.STATUS, data.mInfo.getProfile()) .record(); synchronized (mDeviceStateLock) { - postBluetoothA2dpDeviceConfigChange(data.mNewDevice); + postBluetoothDeviceConfigChange(createBtDeviceInfo(data, data.mNewDevice, + BluetoothProfile.STATE_CONNECTED)); } } else { synchronized (mDeviceStateLock) { @@ -1050,8 +1064,8 @@ import java.util.concurrent.atomic.AtomicBoolean; new AudioModeInfo(mode, pid, uid)); } - /*package*/ void postBluetoothA2dpDeviceConfigChange(@NonNull BluetoothDevice device) { - sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONFIG_CHANGE, SENDMSG_QUEUE, device); + /*package*/ void postBluetoothDeviceConfigChange(@NonNull BtDeviceInfo info) { + sendLMsgNoDelay(MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE, SENDMSG_QUEUE, info); } /*package*/ void startBluetoothScoForClient(IBinder cb, int pid, int scoAudioMode, @@ -1308,6 +1322,10 @@ import java.util.concurrent.atomic.AtomicBoolean; sendIMsgNoDelay(MSG_I_SCO_AUDIO_STATE_CHANGED, SENDMSG_QUEUE, state); } + /*package*/ void postNotifyPreferredAudioProfileApplied(BluetoothDevice btDevice) { + sendLMsgNoDelay(MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED, SENDMSG_QUEUE, btDevice); + } + /*package*/ static final class CommunicationDeviceInfo { final @NonNull IBinder mCb; // Identifies the requesting client for death handler final int mPid; // Requester process ID @@ -1383,9 +1401,11 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - /*package*/ boolean handleDeviceConnection(AudioDeviceAttributes attributes, boolean connect) { + /*package*/ boolean handleDeviceConnection(AudioDeviceAttributes attributes, + boolean connect, @Nullable BluetoothDevice btDevice) { synchronized (mDeviceStateLock) { - return mDeviceInventory.handleDeviceConnection(attributes, connect, false /*for test*/); + return mDeviceInventory.handleDeviceConnection( + attributes, connect, false /*for test*/, btDevice); } } @@ -1626,13 +1646,10 @@ import java.util.concurrent.atomic.AtomicBoolean; (String) msg.obj, msg.arg1); } break; - case MSG_L_A2DP_DEVICE_CONFIG_CHANGE: - final BluetoothDevice btDevice = (BluetoothDevice) msg.obj; + case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE: synchronized (mDeviceStateLock) { - final int a2dpCodec = mBtHelper.getA2dpCodec(btDevice); - mDeviceInventory.onBluetoothA2dpDeviceConfigChange( - new BtHelper.BluetoothA2dpDeviceInfo(btDevice, -1, a2dpCodec), - BtHelper.EVENT_DEVICE_CONFIG_CHANGE); + mDeviceInventory.onBluetoothDeviceConfigChange( + (BtDeviceInfo) msg.obj, BtHelper.EVENT_DEVICE_CONFIG_CHANGE); } break; case MSG_BROADCAST_AUDIO_BECOMING_NOISY: @@ -1790,6 +1807,10 @@ import java.util.concurrent.atomic.AtomicBoolean; final int capturePreset = msg.arg1; mDeviceInventory.onSaveClearPreferredDevicesForCapturePreset(capturePreset); } break; + case MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED: { + final BluetoothDevice btDevice = (BluetoothDevice) msg.obj; + BtHelper.onNotifyPreferredAudioProfileApplied(btDevice); + } break; default: Log.wtf(TAG, "Invalid message " + msg.what); } @@ -1825,7 +1846,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_IL_BTA2DP_TIMEOUT = 10; // process change of A2DP device configuration, obj is BluetoothDevice - private static final int MSG_L_A2DP_DEVICE_CONFIG_CHANGE = 11; + private static final int MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE = 11; private static final int MSG_BROADCAST_AUDIO_BECOMING_NOISY = 12; private static final int MSG_REPORT_NEW_ROUTES = 13; @@ -1865,13 +1886,15 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY = 48; private static final int MSG_IL_BTLEAUDIO_TIMEOUT = 49; + private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 50; + private static boolean isMessageHandledUnderWakelock(int msgId) { switch(msgId) { case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE: case MSG_L_SET_BT_ACTIVE_DEVICE: case MSG_IL_BTA2DP_TIMEOUT: case MSG_IL_BTLEAUDIO_TIMEOUT: - case MSG_L_A2DP_DEVICE_CONFIG_CHANGE: + case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE: case MSG_TOGGLE_HDMI: case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT: case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT: @@ -1963,7 +1986,7 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE: case MSG_IL_BTA2DP_TIMEOUT: case MSG_IL_BTLEAUDIO_TIMEOUT: - case MSG_L_A2DP_DEVICE_CONFIG_CHANGE: + case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE: if (sLastDeviceConnectMsgTime >= time) { // add a little delay to make sure messages are ordered as expected time = sLastDeviceConnectMsgTime + 30; @@ -1983,7 +2006,7 @@ import java.util.concurrent.atomic.AtomicBoolean; static { MESSAGES_MUTE_MUSIC = new HashSet<>(); MESSAGES_MUTE_MUSIC.add(MSG_L_SET_BT_ACTIVE_DEVICE); - MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONFIG_CHANGE); + MESSAGES_MUTE_MUSIC.add(MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE); MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT); MESSAGES_MUTE_MUSIC.add(MSG_IIL_SET_FORCE_BT_A2DP_USE); } @@ -2004,7 +2027,7 @@ import java.util.concurrent.atomic.AtomicBoolean; // Do not mute on bluetooth event if music is playing on a wired headset. if ((message == MSG_L_SET_BT_ACTIVE_DEVICE || message == MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT - || message == MSG_L_A2DP_DEVICE_CONFIG_CHANGE) + || message == MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE) && AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0) && hasIntersection(mDeviceInventory.DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET, mAudioService.getDeviceSetForStream(AudioSystem.STREAM_MUSIC))) { @@ -2143,18 +2166,19 @@ import java.util.concurrent.atomic.AtomicBoolean; if (preferredCommunicationDevice == null) { AudioDeviceAttributes defaultDevice = getDefaultCommunicationDevice(); if (defaultDevice != null) { - setPreferredDevicesForStrategySync( + mDeviceInventory.setPreferredDevicesForStrategy( mCommunicationStrategyId, Arrays.asList(defaultDevice)); - setPreferredDevicesForStrategySync( + mDeviceInventory.setPreferredDevicesForStrategy( mAccessibilityStrategyId, Arrays.asList(defaultDevice)); } else { - removePreferredDevicesForStrategySync(mCommunicationStrategyId); - removePreferredDevicesForStrategySync(mAccessibilityStrategyId); + mDeviceInventory.removePreferredDevicesForStrategy(mCommunicationStrategyId); + mDeviceInventory.removePreferredDevicesForStrategy(mAccessibilityStrategyId); } + mDeviceInventory.applyConnectedDevicesRoles(); } else { - setPreferredDevicesForStrategySync( + mDeviceInventory.setPreferredDevicesForStrategy( mCommunicationStrategyId, Arrays.asList(preferredCommunicationDevice)); - setPreferredDevicesForStrategySync( + mDeviceInventory.setPreferredDevicesForStrategy( mAccessibilityStrategyId, Arrays.asList(preferredCommunicationDevice)); } onUpdatePhoneStrategyDevice(preferredCommunicationDevice); diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 228bc87cc20b9..1eb39f7352afb 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -34,26 +34,36 @@ import android.media.ICapturePresetDevicesRoleDispatcher; import android.media.IStrategyNonDefaultDevicesDispatcher; import android.media.IStrategyPreferredDevicesDispatcher; import android.media.MediaMetrics; +import android.media.MediaRecorder.AudioSource; +import android.media.audiopolicy.AudioProductStrategy; import android.media.permission.ClearCallingIdentityContext; import android.media.permission.SafeCloseable; import android.os.Binder; +import android.os.Bundle; import android.os.RemoteCallbackList; import android.os.RemoteException; +import android.os.SystemProperties; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; +import android.util.Pair; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.utils.EventLogger; +import com.google.android.collect.Sets; + import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -175,18 +185,26 @@ public class AudioDeviceInventory { final RemoteCallbackList mDevRoleCapturePresetDispatchers = new RemoteCallbackList(); + final List mStrategies; + /*package*/ AudioDeviceInventory(@NonNull AudioDeviceBroker broker) { - mDeviceBroker = broker; - mAudioSystem = AudioSystemAdapter.getDefaultAdapter(); + this(broker, AudioSystemAdapter.getDefaultAdapter()); } //----------------------------------------------------------- /** for mocking only, allows to inject AudioSystem adapter */ /*package*/ AudioDeviceInventory(@NonNull AudioSystemAdapter audioSystem) { - mDeviceBroker = null; - mAudioSystem = audioSystem; + this(null, audioSystem); } + private AudioDeviceInventory(@Nullable AudioDeviceBroker broker, + @Nullable AudioSystemAdapter audioSystem) { + mDeviceBroker = broker; + mAudioSystem = audioSystem; + mStrategies = AudioProductStrategy.getAudioProductStrategies(); + mBluetoothDualModeEnabled = SystemProperties.getBoolean( + "persist.bluetooth.enable_dual_mode_audio", false); + } /*package*/ void setDeviceBroker(@NonNull AudioDeviceBroker broker) { mDeviceBroker = broker; } @@ -203,8 +221,13 @@ public class AudioDeviceInventory { int mDeviceCodecFormat; final UUID mSensorUuid; + /** Disabled operating modes for this device. Use a negative logic so that by default + * an empty list means all modes are allowed. + * See BluetoothAdapter.AUDIO_MODE_DUPLEX and BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY */ + @NonNull ArraySet mDisabledModes = new ArraySet(0); + DeviceInfo(int deviceType, String deviceName, String deviceAddress, - int deviceCodecFormat, UUID sensorUuid) { + int deviceCodecFormat, @Nullable UUID sensorUuid) { mDeviceType = deviceType; mDeviceName = deviceName == null ? "" : deviceName; mDeviceAddress = deviceAddress == null ? "" : deviceAddress; @@ -212,11 +235,31 @@ public class AudioDeviceInventory { mSensorUuid = sensorUuid; } + void setModeDisabled(String mode) { + mDisabledModes.add(mode); + } + void setModeEnabled(String mode) { + mDisabledModes.remove(mode); + } + boolean isModeEnabled(String mode) { + return !mDisabledModes.contains(mode); + } + boolean isOutputOnlyModeEnabled() { + return isModeEnabled(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY); + } + boolean isDuplexModeEnabled() { + return isModeEnabled(BluetoothAdapter.AUDIO_MODE_DUPLEX); + } + DeviceInfo(int deviceType, String deviceName, String deviceAddress, int deviceCodecFormat) { this(deviceType, deviceName, deviceAddress, deviceCodecFormat, null); } + DeviceInfo(int deviceType, String deviceName, String deviceAddress) { + this(deviceType, deviceName, deviceAddress, AudioSystem.AUDIO_FORMAT_DEFAULT); + } + @Override public String toString() { return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType) @@ -224,7 +267,8 @@ public class AudioDeviceInventory { + ") name:" + mDeviceName + " addr:" + mDeviceAddress + " codec: " + Integer.toHexString(mDeviceCodecFormat) - + " sensorUuid: " + Objects.toString(mSensorUuid) + "]"; + + " sensorUuid: " + Objects.toString(mSensorUuid) + + " disabled modes: " + mDisabledModes + "]"; } @NonNull String getKey() { @@ -276,9 +320,18 @@ public class AudioDeviceInventory { pw.println(" " + prefix + " type:0x" + Integer.toHexString(keyType) + " (" + AudioSystem.getDeviceName(keyType) + ") addr:" + valueAddress); }); + pw.println("\n" + prefix + "Preferred devices for capture preset:"); mPreferredDevicesForCapturePreset.forEach((capturePreset, devices) -> { pw.println(" " + prefix + "capturePreset:" + capturePreset + " devices:" + devices); }); + pw.println("\n" + prefix + "Applied devices roles for strategies:"); + mAppliedStrategyRoles.forEach((key, devices) -> { + pw.println(" " + prefix + "strategy: " + key.first + + " role:" + key.second + " devices:" + devices); }); + pw.println("\n" + prefix + "Applied devices roles for presets:"); + mAppliedPresetRoles.forEach((key, devices) -> { + pw.println(" " + prefix + "preset: " + key.first + + " role:" + key.second + " devices:" + devices); }); } //------------------------------------------------------------ @@ -299,15 +352,16 @@ public class AudioDeviceInventory { AudioSystem.DEVICE_STATE_AVAILABLE, di.mDeviceCodecFormat); } + mAppliedStrategyRoles.clear(); + applyConnectedDevicesRoles_l(); } synchronized (mPreferredDevices) { mPreferredDevices.forEach((strategy, devices) -> { - mAudioSystem.setDevicesRoleForStrategy( - strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices); }); + setPreferredDevicesForStrategy(strategy, devices); }); } synchronized (mNonDefaultDevices) { mNonDefaultDevices.forEach((strategy, devices) -> { - mAudioSystem.setDevicesRoleForStrategy( + addDevicesRoleForStrategy( strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices); }); } synchronized (mPreferredDevicesForCapturePreset) { @@ -380,8 +434,7 @@ public class AudioDeviceInventory { btInfo.mVolume * 10, btInfo.mAudioSystemDevice, "onSetBtActiveDevice"); } - makeA2dpDeviceAvailable(address, BtHelper.getName(btInfo.mDevice), - "onSetBtActiveDevice", btInfo.mCodec); + makeA2dpDeviceAvailable(btInfo, "onSetBtActiveDevice"); } break; case BluetoothProfile.HEARING_AID: @@ -397,10 +450,7 @@ public class AudioDeviceInventory { if (switchToUnavailable) { makeLeAudioDeviceUnavailableNow(address, btInfo.mAudioSystemDevice); } else if (switchToAvailable) { - makeLeAudioDeviceAvailable(address, BtHelper.getName(btInfo.mDevice), - streamType, btInfo.mVolume == -1 ? -1 : btInfo.mVolume * 10, - btInfo.mAudioSystemDevice, - "onSetBtActiveDevice"); + makeLeAudioDeviceAvailable(btInfo, streamType, "onSetBtActiveDevice"); } break; default: throw new IllegalArgumentException("Invalid profile " @@ -411,30 +461,30 @@ public class AudioDeviceInventory { @GuardedBy("AudioDeviceBroker.mDeviceStateLock") - /*package*/ void onBluetoothA2dpDeviceConfigChange( - @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int event) { + /*package*/ void onBluetoothDeviceConfigChange( + @NonNull AudioDeviceBroker.BtDeviceInfo btInfo, int event) { MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId - + "onBluetoothA2dpDeviceConfigChange") - .set(MediaMetrics.Property.EVENT, BtHelper.a2dpDeviceEventToString(event)); + + "onBluetoothDeviceConfigChange") + .set(MediaMetrics.Property.EVENT, BtHelper.deviceEventToString(event)); - final BluetoothDevice btDevice = btInfo.getBtDevice(); + final BluetoothDevice btDevice = btInfo.mDevice; if (btDevice == null) { mmi.set(MediaMetrics.Property.EARLY_RETURN, "btDevice null").record(); return; } if (AudioService.DEBUG_DEVICES) { - Log.d(TAG, "onBluetoothA2dpDeviceConfigChange btDevice=" + btDevice); + Log.d(TAG, "onBluetoothDeviceConfigChange btDevice=" + btDevice); } - int a2dpVolume = btInfo.getVolume(); - @AudioSystem.AudioFormatNativeEnumForBtCodec final int a2dpCodec = btInfo.getCodec(); + int volume = btInfo.mVolume; + @AudioSystem.AudioFormatNativeEnumForBtCodec final int audioCodec = btInfo.mCodec; String address = btDevice.getAddress(); if (!BluetoothAdapter.checkBluetoothAddress(address)) { address = ""; } AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "onBluetoothA2dpDeviceConfigChange addr=" + address - + " event=" + BtHelper.a2dpDeviceEventToString(event))); + "onBluetoothDeviceConfigChange addr=" + address + + " event=" + BtHelper.deviceEventToString(event))); synchronized (mDevicesLock) { if (mDeviceBroker.hasScheduledA2dpConnection(btDevice)) { @@ -449,53 +499,54 @@ public class AudioDeviceInventory { AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); final DeviceInfo di = mConnectedDevices.get(key); if (di == null) { - Log.e(TAG, "invalid null DeviceInfo in onBluetoothA2dpDeviceConfigChange"); + Log.e(TAG, "invalid null DeviceInfo in onBluetoothDeviceConfigChange"); mmi.set(MediaMetrics.Property.EARLY_RETURN, "null DeviceInfo").record(); return; } mmi.set(MediaMetrics.Property.ADDRESS, address) .set(MediaMetrics.Property.ENCODING, - AudioSystem.audioFormatToString(a2dpCodec)) - .set(MediaMetrics.Property.INDEX, a2dpVolume) + AudioSystem.audioFormatToString(audioCodec)) + .set(MediaMetrics.Property.INDEX, volume) .set(MediaMetrics.Property.NAME, di.mDeviceName); - if (event == BtHelper.EVENT_ACTIVE_DEVICE_CHANGE) { - // Device is connected - if (a2dpVolume != -1) { - mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC, - // convert index to internal representation in VolumeStreamState - a2dpVolume * 10, - AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, - "onBluetoothA2dpDeviceConfigChange"); - } - } else if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) { - if (di.mDeviceCodecFormat != a2dpCodec) { - di.mDeviceCodecFormat = a2dpCodec; - mConnectedDevices.replace(key, di); - } - } - final int res = mAudioSystem.handleDeviceConfigChange( - AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, - BtHelper.getName(btDevice), a2dpCodec); - if (res != AudioSystem.AUDIO_STATUS_OK) { - AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "APM handleDeviceConfigChange failed for A2DP device addr=" + address - + " codec=" + AudioSystem.audioFormatToString(a2dpCodec)) - .printLog(TAG)); + if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) { + boolean a2dpCodecChange = false; + if (btInfo.mProfile == BluetoothProfile.A2DP) { + if (di.mDeviceCodecFormat != audioCodec) { + di.mDeviceCodecFormat = audioCodec; + mConnectedDevices.replace(key, di); + a2dpCodecChange = true; + } + final int res = mAudioSystem.handleDeviceConfigChange( + btInfo.mAudioSystemDevice, address, + BtHelper.getName(btDevice), audioCodec); - int musicDevice = mDeviceBroker.getDeviceForStream(AudioSystem.STREAM_MUSIC); - // force A2DP device disconnection in case of error so that AudioService state is - // consistent with audio policy manager state - setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btDevice, - BluetoothProfile.A2DP, BluetoothProfile.STATE_DISCONNECTED, - musicDevice, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP)); - } else { - AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "APM handleDeviceConfigChange success for A2DP device addr=" + address - + " codec=" + AudioSystem.audioFormatToString(a2dpCodec)) - .printLog(TAG)); + if (res != AudioSystem.AUDIO_STATUS_OK) { + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( + "APM handleDeviceConfigChange failed for A2DP device addr=" + + address + " codec=" + + AudioSystem.audioFormatToString(audioCodec)) + .printLog(TAG)); + + // force A2DP device disconnection in case of error so that AudioService + // state is consistent with audio policy manager state + setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btInfo, + BluetoothProfile.STATE_DISCONNECTED)); + } else { + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( + "APM handleDeviceConfigChange success for A2DP device addr=" + + address + + " codec=" + AudioSystem.audioFormatToString(audioCodec)) + .printLog(TAG)); + + } + } + if (!a2dpCodecChange) { + updateBluetoothPreferredModes_l(); + mDeviceBroker.postNotifyPreferredAudioProfileApplied(btDevice); + } } } mmi.record(); @@ -578,7 +629,7 @@ public class AudioDeviceInventory { } if (!handleDeviceConnection(wdcs.mAttributes, - wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest)) { + wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest, null)) { // change of connection state failed, bailout mmi.set(MediaMetrics.Property.EARLY_RETURN, "change of connection state failed") .record(); @@ -712,23 +763,35 @@ public class AudioDeviceInventory { /*package*/ int setPreferredDevicesForStrategySync(int strategy, @NonNull List devices) { - int status = AudioSystem.ERROR; - - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( - "setPreferredDevicesForStrategySync, strategy: " + strategy - + " devices: " + devices)).printLog(TAG)); - status = mAudioSystem.setDevicesRoleForStrategy( - strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices); - } - + final int status = setPreferredDevicesForStrategy(strategy, devices); if (status == AudioSystem.SUCCESS) { mDeviceBroker.postSaveSetPreferredDevicesForStrategy(strategy, devices); } return status; } + /*package*/ int setPreferredDevicesForStrategy(int strategy, + @NonNull List devices) { + int status = AudioSystem.ERROR; + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( + "setPreferredDevicesForStrategy, strategy: " + strategy + + " devices: " + devices)).printLog(TAG)); + status = setDevicesRoleForStrategy( + strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices); + } + return status; + } + /*package*/ int removePreferredDevicesForStrategySync(int strategy) { + final int status = removePreferredDevicesForStrategy(strategy); + if (status == AudioSystem.SUCCESS) { + mDeviceBroker.postSaveRemovePreferredDevicesForStrategy(strategy); + } + return status; + } + + /*package*/ int removePreferredDevicesForStrategy(int strategy) { int status = AudioSystem.ERROR; try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { @@ -736,13 +799,9 @@ public class AudioDeviceInventory { "removePreferredDevicesForStrategySync, strategy: " + strategy)).printLog(TAG)); - status = mAudioSystem.clearDevicesRoleForStrategy( + status = clearDevicesRoleForStrategy( strategy, AudioSystem.DEVICE_ROLE_PREFERRED); } - - if (status == AudioSystem.SUCCESS) { - mDeviceBroker.postSaveRemovePreferredDevicesForStrategy(strategy); - } return status; } @@ -757,7 +816,7 @@ public class AudioDeviceInventory { AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "setDeviceAsNonDefaultForStrategySync, strategy: " + strategy + " device: " + device)).printLog(TAG)); - status = mAudioSystem.setDevicesRoleForStrategy( + status = addDevicesRoleForStrategy( strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices); } @@ -779,7 +838,7 @@ public class AudioDeviceInventory { "removeDeviceAsNonDefaultForStrategySync, strategy: " + strategy + " devices: " + device)).printLog(TAG)); - status = mAudioSystem.removeDevicesRoleForStrategy( + status = removeDevicesRoleForStrategy( strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices); } @@ -812,33 +871,70 @@ public class AudioDeviceInventory { /*package*/ int setPreferredDevicesForCapturePresetSync( int capturePreset, @NonNull List devices) { - int status = AudioSystem.ERROR; - - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - status = mAudioSystem.setDevicesRoleForCapturePreset( - capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices); - } - + final int status = setPreferredDevicesForCapturePreset(capturePreset, devices); if (status == AudioSystem.SUCCESS) { mDeviceBroker.postSaveSetPreferredDevicesForCapturePreset(capturePreset, devices); } return status; } - /*package*/ int clearPreferredDevicesForCapturePresetSync(int capturePreset) { - int status = AudioSystem.ERROR; - + private int setPreferredDevicesForCapturePreset( + int capturePreset, @NonNull List devices) { + int status = AudioSystem.ERROR; try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - status = mAudioSystem.clearDevicesRoleForCapturePreset( - capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED); + status = setDevicesRoleForCapturePreset( + capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices); } + return status; + } + /*package*/ int clearPreferredDevicesForCapturePresetSync(int capturePreset) { + final int status = clearPreferredDevicesForCapturePreset(capturePreset); if (status == AudioSystem.SUCCESS) { mDeviceBroker.postSaveClearPreferredDevicesForCapturePreset(capturePreset); } return status; } + private int clearPreferredDevicesForCapturePreset(int capturePreset) { + int status = AudioSystem.ERROR; + + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + status = clearDevicesRoleForCapturePreset( + capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED); + } + return status; + } + + private int addDevicesRoleForCapturePreset(int capturePreset, int role, + @NonNull List devices) { + return addDevicesRole(mAppliedPresetRoles, (p, r, d) -> { + return mAudioSystem.addDevicesRoleForCapturePreset(p, r, d); + }, capturePreset, role, devices); + } + + private int removeDevicesRoleForCapturePreset(int capturePreset, int role, + @NonNull List devices) { + return removeDevicesRole(mAppliedPresetRoles, (p, r, d) -> { + return mAudioSystem.removeDevicesRoleForCapturePreset(p, r, d); + }, capturePreset, role, devices); + } + + private int setDevicesRoleForCapturePreset(int capturePreset, int role, + @NonNull List devices) { + return setDevicesRole(mAppliedPresetRoles, (p, r, d) -> { + return mAudioSystem.addDevicesRoleForCapturePreset(p, r, d); + }, (p, r, d) -> { + return mAudioSystem.clearDevicesRoleForCapturePreset(p, r); + }, capturePreset, role, devices); + } + + private int clearDevicesRoleForCapturePreset(int capturePreset, int role) { + return clearDevicesRole(mAppliedPresetRoles, (p, r, d) -> { + return mAudioSystem.clearDevicesRoleForCapturePreset(p, r); + }, capturePreset, role); + } + /*package*/ void registerCapturePresetDevicesRoleDispatcher( @NonNull ICapturePresetDevicesRoleDispatcher dispatcher) { mDevRoleCapturePresetDispatchers.register(dispatcher); @@ -849,7 +945,208 @@ public class AudioDeviceInventory { mDevRoleCapturePresetDispatchers.unregister(dispatcher); } - //----------------------------------------------------------------------- + private int addDevicesRoleForStrategy(int strategy, int role, + @NonNull List devices) { + return addDevicesRole(mAppliedStrategyRoles, (s, r, d) -> { + return mAudioSystem.setDevicesRoleForStrategy(s, r, d); + }, strategy, role, devices); + } + + private int removeDevicesRoleForStrategy(int strategy, int role, + @NonNull List devices) { + return removeDevicesRole(mAppliedStrategyRoles, (s, r, d) -> { + return mAudioSystem.removeDevicesRoleForStrategy(s, r, d); + }, strategy, role, devices); + } + + private int setDevicesRoleForStrategy(int strategy, int role, + @NonNull List devices) { + return setDevicesRole(mAppliedStrategyRoles, (s, r, d) -> { + return mAudioSystem.setDevicesRoleForStrategy(s, r, d); + }, (s, r, d) -> { + return mAudioSystem.clearDevicesRoleForStrategy(s, r); + }, strategy, role, devices); + } + + private int clearDevicesRoleForStrategy(int strategy, int role) { + return clearDevicesRole(mAppliedStrategyRoles, (s, r, d) -> { + return mAudioSystem.clearDevicesRoleForStrategy(s, r); + }, strategy, role); + } + + //------------------------------------------------------------ + // Cache for applied roles for strategies and devices. The cache avoids reapplying the + // same list of devices for a given role and strategy and the corresponding systematic + // redundant work in audio policy manager and audio flinger. + // The key is the pair and the value is the current list of devices. + + private final ArrayMap, List> + mAppliedStrategyRoles = new ArrayMap<>(); + + // Cache for applied roles for capture presets and devices. The cache avoids reapplying the + // same list of devices for a given role and capture preset and the corresponding systematic + // redundant work in audio policy manager and audio flinger. + // The key is the pair and the value is the current list of devices. + private final ArrayMap, List> + mAppliedPresetRoles = new ArrayMap<>(); + + interface AudioSystemInterface { + int deviceRoleAction(int usecase, int role, @Nullable List devices); + } + + private int addDevicesRole( + ArrayMap, List> rolesMap, + AudioSystemInterface asi, + int useCase, int role, @NonNull List devices) { + synchronized (rolesMap) { + Pair key = new Pair<>(useCase, role); + List roleDevices = new ArrayList<>(); + List appliedDevices = new ArrayList<>(); + + if (rolesMap.containsKey(key)) { + roleDevices = rolesMap.get(key); + for (AudioDeviceAttributes device : devices) { + if (!roleDevices.contains(device)) { + appliedDevices.add(device); + } + } + } else { + appliedDevices.addAll(devices); + } + if (appliedDevices.isEmpty()) { + return AudioSystem.SUCCESS; + } + final int status = asi.deviceRoleAction(useCase, role, appliedDevices); + if (status == AudioSystem.SUCCESS) { + roleDevices.addAll(appliedDevices); + rolesMap.put(key, roleDevices); + } + return status; + } + } + + private int removeDevicesRole( + ArrayMap, List> rolesMap, + AudioSystemInterface asi, + int useCase, int role, @NonNull List devices) { + synchronized (rolesMap) { + Pair key = new Pair<>(useCase, role); + if (!rolesMap.containsKey(key)) { + return AudioSystem.SUCCESS; + } + List roleDevices = rolesMap.get(key); + List appliedDevices = new ArrayList<>(); + for (AudioDeviceAttributes device : devices) { + if (roleDevices.contains(device)) { + appliedDevices.add(device); + } + } + if (appliedDevices.isEmpty()) { + return AudioSystem.SUCCESS; + } + final int status = asi.deviceRoleAction(useCase, role, appliedDevices); + if (status == AudioSystem.SUCCESS) { + roleDevices.removeAll(appliedDevices); + if (roleDevices.isEmpty()) { + rolesMap.remove(key); + } else { + rolesMap.put(key, roleDevices); + } + } + return status; + } + } + + private int setDevicesRole( + ArrayMap, List> rolesMap, + AudioSystemInterface addOp, + AudioSystemInterface clearOp, + int useCase, int role, @NonNull List devices) { + synchronized (rolesMap) { + Pair key = new Pair<>(useCase, role); + List roleDevices = new ArrayList<>(); + List appliedDevices = new ArrayList<>(); + + if (rolesMap.containsKey(key)) { + roleDevices = rolesMap.get(key); + boolean equal = false; + if (roleDevices.size() == devices.size()) { + roleDevices.retainAll(devices); + equal = roleDevices.size() == devices.size(); + } + if (!equal) { + clearOp.deviceRoleAction(useCase, role, null); + roleDevices.clear(); + appliedDevices.addAll(devices); + } + } else { + appliedDevices.addAll(devices); + } + if (appliedDevices.isEmpty()) { + return AudioSystem.SUCCESS; + } + final int status = addOp.deviceRoleAction(useCase, role, appliedDevices); + if (status == AudioSystem.SUCCESS) { + roleDevices.addAll(appliedDevices); + rolesMap.put(key, roleDevices); + } + return status; + } + } + + private int clearDevicesRole( + ArrayMap, List> rolesMap, + AudioSystemInterface asi, int useCase, int role) { + synchronized (rolesMap) { + Pair key = new Pair<>(useCase, role); + if (!rolesMap.containsKey(key)) { + return AudioSystem.SUCCESS; + } + final int status = asi.deviceRoleAction(useCase, role, null); + if (status == AudioSystem.SUCCESS) { + rolesMap.remove(key); + } + return status; + } + } + + @GuardedBy("mDevicesLock") + private void purgeDevicesRoles_l() { + purgeRoles(mAppliedStrategyRoles, (s, r, d) -> { + return mAudioSystem.removeDevicesRoleForStrategy(s, r, d); }); + purgeRoles(mAppliedPresetRoles, (p, r, d) -> { + return mAudioSystem.removeDevicesRoleForCapturePreset(p, r, d); }); + } + + @GuardedBy("mDevicesLock") + private void purgeRoles( + ArrayMap, List> rolesMap, + AudioSystemInterface asi) { + synchronized (rolesMap) { + Iterator, List>> itRole = + rolesMap.entrySet().iterator(); + while (itRole.hasNext()) { + Map.Entry, List> entry = + itRole.next(); + Pair keyRole = entry.getKey(); + Iterator itDev = rolesMap.get(keyRole).iterator(); + while (itDev.hasNext()) { + AudioDeviceAttributes ada = itDev.next(); + final String devKey = DeviceInfo.makeDeviceListKey(ada.getInternalType(), + ada.getAddress()); + if (mConnectedDevices.get(devKey) == null) { + asi.deviceRoleAction(keyRole.first, keyRole.second, Arrays.asList(ada)); + itDev.remove(); + } + } + if (rolesMap.get(keyRole).isEmpty()) { + itRole.remove(); + } + } + } + } + +//----------------------------------------------------------------------- /** * Check if a device is in the list of connected devices @@ -871,10 +1168,11 @@ public class AudioDeviceInventory { * @param connect true if connection * @param isForTesting if true, not calling AudioSystem for the connection as this is * just for testing + * @param btDevice the corresponding Bluetooth device when relevant. * @return false if an error was reported by AudioSystem */ /*package*/ boolean handleDeviceConnection(AudioDeviceAttributes attributes, boolean connect, - boolean isForTesting) { + boolean isForTesting, @Nullable BluetoothDevice btDevice) { int device = attributes.getInternalType(); String address = attributes.getAddress(); String deviceName = attributes.getName(); @@ -889,6 +1187,7 @@ public class AudioDeviceInventory { .set(MediaMetrics.Property.MODE, connect ? MediaMetrics.Value.CONNECT : MediaMetrics.Value.DISCONNECT) .set(MediaMetrics.Property.NAME, deviceName); + boolean status = false; synchronized (mDevicesLock) { final String deviceKey = DeviceInfo.makeDeviceListKey(device, address); if (AudioService.DEBUG_DEVICES) { @@ -916,24 +1215,33 @@ public class AudioDeviceInventory { .record(); return false; } - mConnectedDevices.put(deviceKey, new DeviceInfo( - device, deviceName, address, AudioSystem.AUDIO_FORMAT_DEFAULT)); + mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, address)); mDeviceBroker.postAccessoryPlugMediaUnmute(device); - mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record(); - return true; + status = true; } else if (!connect && isConnected) { mAudioSystem.setDeviceConnectionState(attributes, AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); // always remove even if disconnection failed mConnectedDevices.remove(deviceKey); - mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record(); - return true; + status = true; + } + if (status) { + if (AudioSystem.isBluetoothScoDevice(device)) { + updateBluetoothPreferredModes_l(); + if (connect) { + mDeviceBroker.postNotifyPreferredAudioProfileApplied(btDevice); + } else { + purgeDevicesRoles_l(); + } + } + mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record(); + } else { + Log.w(TAG, "handleDeviceConnection() failed, deviceKey=" + deviceKey + + ", deviceSpec=" + di + ", connect=" + connect); + mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.DISCONNECTED).record(); } - Log.w(TAG, "handleDeviceConnection() failed, deviceKey=" + deviceKey - + ", deviceSpec=" + di + ", connect=" + connect); } - mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.DISCONNECTED).record(); - return false; + return status; } @@ -1142,15 +1450,20 @@ public class AudioDeviceInventory { // Internal utilities @GuardedBy("mDevicesLock") - private void makeA2dpDeviceAvailable(String address, String name, String eventSource, - int a2dpCodec) { + private void makeA2dpDeviceAvailable(AudioDeviceBroker.BtDeviceInfo btInfo, + String eventSource) { + final String address = btInfo.mDevice.getAddress(); + final String name = BtHelper.getName(btInfo.mDevice); + final int a2dpCodec = btInfo.mCodec; + // enable A2DP before notifying A2DP connection to avoid unnecessary processing in // audio policy manager mDeviceBroker.setBluetoothA2dpOnInt(true, true /*fromA2dp*/, eventSource); // at this point there could be another A2DP device already connected in APM, but it // doesn't matter as this new one will overwrite the previous one - final int res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes( - AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, name), + AudioDeviceAttributes ada = new AudioDeviceAttributes( + AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, name); + final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, a2dpCodec); // TODO: log in MediaMetrics once distinction between connection failure and @@ -1172,8 +1485,7 @@ public class AudioDeviceInventory { // The convention for head tracking sensors associated with A2DP devices is to // use a UUID derived from the MAC address as follows: // time_low = 0, time_mid = 0, time_hi = 0, clock_seq = 0, node = MAC Address - UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes( - new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address)); + UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada); final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name, address, a2dpCodec, sensorUuid); final String diKey = di.getKey(); @@ -1184,6 +1496,206 @@ public class AudioDeviceInventory { mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP); setCurrentAudioRouteNameIfPossible(name, true /*fromA2dp*/); + + updateBluetoothPreferredModes_l(); + mDeviceBroker.postNotifyPreferredAudioProfileApplied(btInfo.mDevice); + } + + static final int[] CAPTURE_PRESETS = new int[] {AudioSource.MIC, AudioSource.CAMCORDER, + AudioSource.VOICE_RECOGNITION, AudioSource.VOICE_COMMUNICATION, + AudioSource.UNPROCESSED, AudioSource.VOICE_PERFORMANCE, AudioSource.HOTWORD}; + + // reflects system property persist.bluetooth.enable_dual_mode_audio + final boolean mBluetoothDualModeEnabled; + /** + * Goes over all connected Bluetooth devices and set the audio policy device role to DISABLED + * or not according to their own and other devices modes. + * The top priority is given to LE devices, then SCO ,then A2DP. + */ + @GuardedBy("mDevicesLock") + private void applyConnectedDevicesRoles_l() { + if (!mBluetoothDualModeEnabled) { + return; + } + DeviceInfo leOutDevice = + getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_BLE_SET); + DeviceInfo leInDevice = + getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_BLE_SET); + DeviceInfo a2dpDevice = + getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_A2DP_SET); + DeviceInfo scoOutDevice = + getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_SCO_SET); + DeviceInfo scoInDevice = + getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_SCO_SET); + boolean disableA2dp = (leOutDevice != null && leOutDevice.isOutputOnlyModeEnabled()); + boolean disableSco = (leOutDevice != null && leOutDevice.isDuplexModeEnabled()) + || (leInDevice != null && leInDevice.isDuplexModeEnabled()); + AudioDeviceAttributes communicationDevice = + mDeviceBroker.mActiveCommunicationDevice == null + ? null : ((mDeviceBroker.isInCommunication() + && mDeviceBroker.mActiveCommunicationDevice != null) + ? new AudioDeviceAttributes(mDeviceBroker.mActiveCommunicationDevice) + : null); + + if (AudioService.DEBUG_DEVICES) { + Log.i(TAG, "applyConnectedDevicesRoles_l\n - leOutDevice: " + leOutDevice + + "\n - leInDevice: " + leInDevice + + "\n - a2dpDevice: " + a2dpDevice + + "\n - scoOutDevice: " + scoOutDevice + + "\n - scoInDevice: " + scoInDevice + + "\n - disableA2dp: " + disableA2dp + + ", disableSco: " + disableSco); + } + + for (DeviceInfo di : mConnectedDevices.values()) { + if (!AudioSystem.isBluetoothDevice(di.mDeviceType)) { + continue; + } + AudioDeviceAttributes ada = + new AudioDeviceAttributes(di.mDeviceType, di.mDeviceAddress, di.mDeviceName); + if (AudioService.DEBUG_DEVICES) { + Log.i(TAG, " + checking Device: " + ada); + } + if (ada.equalTypeAddress(communicationDevice)) { + continue; + } + + if (AudioSystem.isBluetoothOutDevice(di.mDeviceType)) { + for (AudioProductStrategy strategy : mStrategies) { + boolean disable = false; + if (strategy.getId() == mDeviceBroker.mCommunicationStrategyId) { + if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) { + disable = disableSco || !di.isDuplexModeEnabled(); + } else if (AudioSystem.isBluetoothLeDevice(di.mDeviceType)) { + disable = !di.isDuplexModeEnabled(); + } + } else { + if (AudioSystem.isBluetoothA2dpOutDevice(di.mDeviceType)) { + disable = disableA2dp || !di.isOutputOnlyModeEnabled(); + } else if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) { + disable = disableSco || !di.isOutputOnlyModeEnabled(); + } else if (AudioSystem.isBluetoothLeDevice(di.mDeviceType)) { + disable = !di.isOutputOnlyModeEnabled(); + } + } + if (AudioService.DEBUG_DEVICES) { + Log.i(TAG, " - strategy: " + strategy.getId() + + ", disable: " + disable); + } + if (disable) { + addDevicesRoleForStrategy(strategy.getId(), + AudioSystem.DEVICE_ROLE_DISABLED, Arrays.asList(ada)); + } else { + removeDevicesRoleForStrategy(strategy.getId(), + AudioSystem.DEVICE_ROLE_DISABLED, Arrays.asList(ada)); + } + } + } + if (AudioSystem.isBluetoothInDevice(di.mDeviceType)) { + for (int capturePreset : CAPTURE_PRESETS) { + boolean disable = false; + if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) { + disable = disableSco || !di.isDuplexModeEnabled(); + } else if (AudioSystem.isBluetoothLeDevice(di.mDeviceType)) { + disable = !di.isDuplexModeEnabled(); + } + if (AudioService.DEBUG_DEVICES) { + Log.i(TAG, " - capturePreset: " + capturePreset + + ", disable: " + disable); + } + if (disable) { + addDevicesRoleForCapturePreset(capturePreset, + AudioSystem.DEVICE_ROLE_DISABLED, Arrays.asList(ada)); + } else { + removeDevicesRoleForCapturePreset(capturePreset, + AudioSystem.DEVICE_ROLE_DISABLED, Arrays.asList(ada)); + } + } + } + } + } + + /* package */ void applyConnectedDevicesRoles() { + synchronized (mDevicesLock) { + applyConnectedDevicesRoles_l(); + } + } + + @GuardedBy("mDevicesLock") + int checkProfileIsConnected(int profile) { + switch (profile) { + case BluetoothProfile.HEADSET: + if (getFirstConnectedDeviceOfTypes( + AudioSystem.DEVICE_OUT_ALL_SCO_SET) != null + || getFirstConnectedDeviceOfTypes( + AudioSystem.DEVICE_IN_ALL_SCO_SET) != null) { + return profile; + } + break; + case BluetoothProfile.A2DP: + if (getFirstConnectedDeviceOfTypes( + AudioSystem.DEVICE_OUT_ALL_A2DP_SET) != null) { + return profile; + } + break; + case BluetoothProfile.LE_AUDIO: + case BluetoothProfile.LE_AUDIO_BROADCAST: + if (getFirstConnectedDeviceOfTypes( + AudioSystem.DEVICE_OUT_ALL_BLE_SET) != null + || getFirstConnectedDeviceOfTypes( + AudioSystem.DEVICE_IN_ALL_BLE_SET) != null) { + return profile; + } + break; + default: + break; + } + return 0; + } + + @GuardedBy("mDevicesLock") + private void updateBluetoothPreferredModes_l() { + if (!mBluetoothDualModeEnabled) { + return; + } + HashSet processedAddresses = new HashSet<>(0); + for (DeviceInfo di : mConnectedDevices.values()) { + if (!AudioSystem.isBluetoothDevice(di.mDeviceType) + || processedAddresses.contains(di.mDeviceAddress)) { + continue; + } + Bundle preferredProfiles = BtHelper.getPreferredAudioProfiles(di.mDeviceAddress); + if (AudioService.DEBUG_DEVICES) { + Log.i(TAG, "updateBluetoothPreferredModes_l processing device address: " + + di.mDeviceAddress + ", preferredProfiles: " + preferredProfiles); + } + for (DeviceInfo di2 : mConnectedDevices.values()) { + if (!AudioSystem.isBluetoothDevice(di2.mDeviceType) + || !di.mDeviceAddress.equals(di2.mDeviceAddress)) { + continue; + } + int profile = BtHelper.getProfileFromType(di2.mDeviceType); + if (profile == 0) { + continue; + } + int preferredProfile = checkProfileIsConnected( + preferredProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)); + if (preferredProfile == profile || preferredProfile == 0) { + di2.setModeEnabled(BluetoothAdapter.AUDIO_MODE_DUPLEX); + } else { + di2.setModeDisabled(BluetoothAdapter.AUDIO_MODE_DUPLEX); + } + preferredProfile = checkProfileIsConnected( + preferredProfiles.getInt(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY)); + if (preferredProfile == profile || preferredProfile == 0) { + di2.setModeEnabled(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY); + } else { + di2.setModeDisabled(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY); + } + } + processedAddresses.add(di.mDeviceAddress); + } + applyConnectedDevicesRoles_l(); } @GuardedBy("mDevicesLock") @@ -1231,6 +1743,9 @@ public class AudioDeviceInventory { // Remove A2DP routes as well setCurrentAudioRouteNameIfPossible(null, true /*fromA2dp*/); mmi.record(); + + updateBluetoothPreferredModes_l(); + purgeDevicesRoles_l(); } @GuardedBy("mDevicesLock") @@ -1260,8 +1775,7 @@ public class AudioDeviceInventory { AudioSystem.AUDIO_FORMAT_DEFAULT); mConnectedDevices.put( DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address), - new DeviceInfo(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, "", - address, AudioSystem.AUDIO_FORMAT_DEFAULT)); + new DeviceInfo(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, "", address)); } @GuardedBy("mDevicesLock") @@ -1287,8 +1801,7 @@ public class AudioDeviceInventory { AudioSystem.AUDIO_FORMAT_DEFAULT); mConnectedDevices.put( DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address), - new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name, - address, AudioSystem.AUDIO_FORMAT_DEFAULT)); + new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name, address)); mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_HEARING_AID); mDeviceBroker.postApplyVolumeOnDevice(streamType, AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable"); @@ -1326,29 +1839,56 @@ public class AudioDeviceInventory { * @return true if a DEVICE_OUT_HEARING_AID is connected, false otherwise. */ boolean isHearingAidConnected() { + return getFirstConnectedDeviceOfTypes( + Sets.newHashSet(AudioSystem.DEVICE_OUT_HEARING_AID)) != null; + } + + /** + * Returns a DeviceInfo for the fist connected device matching one of the supplied types + */ + private DeviceInfo getFirstConnectedDeviceOfTypes(Set internalTypes) { + List devices = getConnectedDevicesOfTypes(internalTypes); + return devices.isEmpty() ? null : devices.get(0); + } + + /** + * Returns a list of connected devices matching one one of the supplied types + */ + private List getConnectedDevicesOfTypes(Set internalTypes) { + ArrayList devices = new ArrayList<>(); synchronized (mDevicesLock) { for (DeviceInfo di : mConnectedDevices.values()) { - if (di.mDeviceType == AudioSystem.DEVICE_OUT_HEARING_AID) { - return true; + if (internalTypes.contains(di.mDeviceType)) { + devices.add(di); } } - return false; } + return devices; + } + + /* package */ AudioDeviceAttributes getDeviceOfType(int type) { + DeviceInfo di = getFirstConnectedDeviceOfTypes(Sets.newHashSet(type)); + return di == null ? null : new AudioDeviceAttributes( + di.mDeviceType, di.mDeviceAddress, di.mDeviceName); } @GuardedBy("mDevicesLock") - private void makeLeAudioDeviceAvailable(String address, String name, int streamType, - int volumeIndex, int device, String eventSource) { + private void makeLeAudioDeviceAvailable( + AudioDeviceBroker.BtDeviceInfo btInfo, int streamType, String eventSource) { + final String address = btInfo.mDevice.getAddress(); + final String name = BtHelper.getName(btInfo.mDevice); + final int volumeIndex = btInfo.mVolume == -1 ? -1 : btInfo.mVolume * 10; + final int device = btInfo.mAudioSystemDevice; + if (device != AudioSystem.DEVICE_NONE) { /* Audio Policy sees Le Audio similar to A2DP. Let's make sure * AUDIO_POLICY_FORCE_NO_BT_A2DP is not set */ mDeviceBroker.setBluetoothA2dpOnInt(true, false /*fromA2dp*/, eventSource); - final int res = AudioSystem.setDeviceConnectionState(new AudioDeviceAttributes( - device, address, name), - AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioDeviceAttributes ada = new AudioDeviceAttributes(device, address, name); + final int res = AudioSystem.setDeviceConnectionState(ada, + AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "APM failed to make available LE Audio device addr=" + address @@ -1359,12 +1899,13 @@ public class AudioDeviceInventory { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "LE Audio device addr=" + address + " now available").printLog(TAG)); } - // Reset LEA suspend state each time a new sink is connected mDeviceBroker.clearLeAudioSuspended(); + UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada); mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address), - new DeviceInfo(device, name, address, AudioSystem.AUDIO_FORMAT_DEFAULT)); + new DeviceInfo(device, name, address, AudioSystem.AUDIO_FORMAT_DEFAULT, + sensorUuid)); mDeviceBroker.postAccessoryPlugMediaUnmute(device); setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false); } @@ -1380,6 +1921,9 @@ public class AudioDeviceInventory { final int maxIndex = mDeviceBroker.getMaxVssVolumeForStream(streamType); mDeviceBroker.postSetLeAudioVolumeIndex(leAudioVolIndex, maxIndex, streamType); mDeviceBroker.postApplyVolumeOnDevice(streamType, device, "makeLeAudioDeviceAvailable"); + + updateBluetoothPreferredModes_l(); + mDeviceBroker.postNotifyPreferredAudioProfileApplied(btInfo.mDevice); } @GuardedBy("mDevicesLock") @@ -1404,6 +1948,9 @@ public class AudioDeviceInventory { } setCurrentAudioRouteNameIfPossible(null, false /*fromA2dp*/); + + updateBluetoothPreferredModes_l(); + purgeDevicesRoles_l(); } @GuardedBy("mDevicesLock") @@ -1739,18 +2286,6 @@ public class AudioDeviceInventory { } } - /* package */ AudioDeviceAttributes getDeviceOfType(int type) { - synchronized (mDevicesLock) { - for (DeviceInfo di : mConnectedDevices.values()) { - if (di.mDeviceType == type) { - return new AudioDeviceAttributes( - di.mDeviceType, di.mDeviceAddress, di.mDeviceName); - } - } - } - return null; - } - //---------------------------------------------------------- // For tests only @@ -1761,10 +2296,12 @@ public class AudioDeviceInventory { */ @VisibleForTesting public boolean isA2dpDeviceConnected(@NonNull BluetoothDevice device) { - final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, - device.getAddress()); - synchronized (mDevicesLock) { - return (mConnectedDevices.get(key) != null); + for (DeviceInfo di : getConnectedDevicesOfTypes( + Sets.newHashSet(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP))) { + if (di.mDeviceAddress.equals(device.getAddress())) { + return true; + } } + return false; } } diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index 7af7ed5fff652..1142a8d64304c 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -435,7 +435,7 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, } /** - * Same as {@link AudioSystem#removeDevicesRoleForCapturePreset(int, int, int[], String[])} + * Same as {@link AudioSystem#removeDevicesRoleForCapturePreset(int, int, List)} * @param capturePreset * @param role * @param devicesToRemove @@ -447,6 +447,19 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, return AudioSystem.removeDevicesRoleForCapturePreset(capturePreset, role, devicesToRemove); } + /** + * Same as {@link AudioSystem#addDevicesRoleForCapturePreset(int, int, List)} + * @param capturePreset the capture preset to configure + * @param role the role of the devices + * @param devices the list of devices to be added as role for the given capture preset + * @return {@link #SUCCESS} if successfully add + */ + public int addDevicesRoleForCapturePreset( + int capturePreset, int role, @NonNull List devices) { + invalidateRoutingCache(); + return AudioSystem.addDevicesRoleForCapturePreset(capturePreset, role, devices); + } + /** * Same as {@link AudioSystem#} * @param capturePreset diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 8c27c3ecfd872..e46c3cc4a2853 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -33,6 +33,7 @@ import android.media.AudioManager; import android.media.AudioSystem; import android.media.BluetoothProfileConnectionInfo; import android.os.Binder; +import android.os.Bundle; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; @@ -150,60 +151,12 @@ public class BtHelper { } } - //---------------------------------------------------------------------- - /*package*/ static class BluetoothA2dpDeviceInfo { - private final @NonNull BluetoothDevice mBtDevice; - private final int mVolume; - private final @AudioSystem.AudioFormatNativeEnumForBtCodec int mCodec; - - BluetoothA2dpDeviceInfo(@NonNull BluetoothDevice btDevice) { - this(btDevice, -1, AudioSystem.AUDIO_FORMAT_DEFAULT); - } - - BluetoothA2dpDeviceInfo(@NonNull BluetoothDevice btDevice, int volume, int codec) { - mBtDevice = btDevice; - mVolume = volume; - mCodec = codec; - } - - public @NonNull BluetoothDevice getBtDevice() { - return mBtDevice; - } - - public int getVolume() { - return mVolume; - } - - public @AudioSystem.AudioFormatNativeEnumForBtCodec int getCodec() { - return mCodec; - } - - // redefine equality op so we can match messages intended for this device - @Override - public boolean equals(Object o) { - if (o == null) { - return false; - } - if (this == o) { - return true; - } - if (o instanceof BluetoothA2dpDeviceInfo) { - return mBtDevice.equals(((BluetoothA2dpDeviceInfo) o).getBtDevice()); - } - return false; - } - - - } - // A2DP device events /*package*/ static final int EVENT_DEVICE_CONFIG_CHANGE = 0; - /*package*/ static final int EVENT_ACTIVE_DEVICE_CHANGE = 1; - /*package*/ static String a2dpDeviceEventToString(int event) { + /*package*/ static String deviceEventToString(int event) { switch (event) { case EVENT_DEVICE_CONFIG_CHANGE: return "DEVICE_CONFIG_CHANGE"; - case EVENT_ACTIVE_DEVICE_CHANGE: return "ACTIVE_DEVICE_CHANGE"; default: return new String("invalid event:" + event); } @@ -620,11 +573,12 @@ public class BtHelper { return btHeadsetDeviceToAudioDevice(mBluetoothHeadsetDevice); } - private AudioDeviceAttributes btHeadsetDeviceToAudioDevice(BluetoothDevice btDevice) { + private static AudioDeviceAttributes btHeadsetDeviceToAudioDevice(BluetoothDevice btDevice) { if (btDevice == null) { return new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, ""); } String address = btDevice.getAddress(); + String name = getName(btDevice); if (!BluetoothAdapter.checkBluetoothAddress(address)) { address = ""; } @@ -646,7 +600,7 @@ public class BtHelper { + " btClass: " + (btClass == null ? "Unknown" : btClass) + " nativeType: " + nativeType + " address: " + address); } - return new AudioDeviceAttributes(nativeType, address); + return new AudioDeviceAttributes(nativeType, address, name); } private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) { @@ -655,12 +609,9 @@ public class BtHelper { } int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET; AudioDeviceAttributes audioDevice = btHeadsetDeviceToAudioDevice(btDevice); - String btDeviceName = getName(btDevice); boolean result = false; if (isActive) { - result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( - audioDevice.getInternalType(), audioDevice.getAddress(), btDeviceName), - isActive); + result |= mDeviceBroker.handleDeviceConnection(audioDevice, isActive, btDevice); } else { int[] outDeviceTypes = { AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, @@ -669,14 +620,14 @@ public class BtHelper { }; for (int outDeviceType : outDeviceTypes) { result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( - outDeviceType, audioDevice.getAddress(), btDeviceName), - isActive); + outDeviceType, audioDevice.getAddress(), audioDevice.getName()), + isActive, btDevice); } } // handleDeviceConnection() && result to make sure the method get executed result = mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( - inDevice, audioDevice.getAddress(), btDeviceName), - isActive) && result; + inDevice, audioDevice.getAddress(), audioDevice.getName()), + isActive, btDevice) && result; return result; } @@ -973,6 +924,30 @@ public class BtHelper { } } + /*package */ static int getProfileFromType(int deviceType) { + if (AudioSystem.isBluetoothA2dpOutDevice(deviceType)) { + return BluetoothProfile.A2DP; + } else if (AudioSystem.isBluetoothScoDevice(deviceType)) { + return BluetoothProfile.HEADSET; + } else if (AudioSystem.isBluetoothLeDevice(deviceType)) { + return BluetoothProfile.LE_AUDIO; + } + return 0; // 0 is not a valid profile + } + + /*package */ static Bundle getPreferredAudioProfiles(String address) { + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + return adapter.getPreferredAudioProfiles(adapter.getRemoteDevice(address)); + } + + /** + * Notifies Bluetooth framework that new preferred audio profiles for Bluetooth devices + * have been applied. + */ + public static void onNotifyPreferredAudioProfileApplied(BluetoothDevice btDevice) { + BluetoothAdapter.getDefaultAdapter().notifyActiveDeviceChangeApplied(btDevice); + } + /** * Returns the string equivalent for the btDeviceClass class. */