[DO NOT MERGE] AudioService: fix disconnect/connect of A2DP device

Due to the expected BECOMING_NOISY behavior associated
with a device disconnection, the disconnection is handled
asynchronously after a fixed delay. This delay caused an
inversion of commands in the processing order of the
disconnection of a device closely followed by connection
of the same device.
 The fix consists in:
- overriding the equals() operator for BtDeviceConnectionInfo
 so messages for a given device in the message queue
 can be checked / removed.
- when AudioDeviceBroker receives a command for A2DP
 connection or disconnection, remove all upcoming connection
 and disconnection commands in the queue for this device
 (see postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent)
- remove AudioDeviceBroker.handleSetA2dpSinkConnectionState, which
 was only used in BtHelper.onA2dpProfileConnected() with
 a CONNECTED state, and have this method perform a regular device
 connection (just like when coming from AM->AS).
- in AudioDeviceInventory.onSetA2dpSinkConnectionState(), support
 receiving a connection event for an already connected device,
 to support codec changes.

This change also includes modifications to the classes involved
in the device connection to make them support mocking/spying
to reproduce the bug conditions (see AudioDeviceBrokerTest).

Bug: 134932649
Test: atest AudioDeviceBrokerTest
Change-Id: If2b3b41409c77467a181a2f9b42310db9b9de8c5
This commit is contained in:
Jean-Michel Trivi
2019-06-24 10:39:19 -07:00
parent dbe49843de
commit 8c92bde392
6 changed files with 345 additions and 79 deletions

View File

@@ -15,9 +15,6 @@
*/ */
package com.android.server.audio; package com.android.server.audio;
import static com.android.server.audio.AudioService.CONNECTION_STATE_CONNECTED;
import static com.android.server.audio.AudioService.CONNECTION_STATE_DISCONNECTED;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
@@ -93,13 +90,28 @@ import com.android.internal.annotations.GuardedBy;
/*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service) { /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service) {
mContext = context; mContext = context;
mAudioService = service; mAudioService = service;
setupMessaging(context);
mBtHelper = new BtHelper(this); mBtHelper = new BtHelper(this);
mDeviceInventory = new AudioDeviceInventory(this); mDeviceInventory = new AudioDeviceInventory(this);
init();
}
/** for test purposes only, inject AudioDeviceInventory */
AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service,
@NonNull AudioDeviceInventory mockDeviceInventory) {
mContext = context;
mAudioService = service;
mBtHelper = new BtHelper(this);
mDeviceInventory = mockDeviceInventory;
init();
}
private void init() {
setupMessaging(mContext);
mForcedUseForComm = AudioSystem.FORCE_NONE; mForcedUseForComm = AudioSystem.FORCE_NONE;
mForcedUseForCommExt = mForcedUseForComm; mForcedUseForCommExt = mForcedUseForComm;
} }
/*package*/ Context getContext() { /*package*/ Context getContext() {
@@ -230,17 +242,42 @@ import com.android.internal.annotations.GuardedBy;
mSupprNoisy = suppressNoisyIntent; mSupprNoisy = suppressNoisyIntent;
mVolume = vol; mVolume = vol;
} }
// redefine equality op so we can match messages intended for this device
@Override
public boolean equals(Object o) {
return mDevice.equals(o);
}
} }
/*package*/ void postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent( /*package*/ void postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
@NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state, @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
int profile, boolean suppressNoisyIntent, int a2dpVolume) { int profile, boolean suppressNoisyIntent, int a2dpVolume) {
final BtDeviceConnectionInfo info = new BtDeviceConnectionInfo(device, state, profile, final BtDeviceConnectionInfo info = new BtDeviceConnectionInfo(device, state, profile,
suppressNoisyIntent, a2dpVolume); suppressNoisyIntent, a2dpVolume);
// TODO add a check to try to remove unprocessed messages for the same device (the old // when receiving a request to change the connection state of a device, this last request
// check didn't work), and make sure it doesn't conflict with config change message // is the source of truth, so cancel all previous requests
sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info); removeAllA2dpConnectionEvents(device);
sendLMsgNoDelay(
state == BluetoothProfile.STATE_CONNECTED
? MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION
: MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
SENDMSG_QUEUE, info);
}
/** remove all previously scheduled connection and disconnection events for the given device */
private void removeAllA2dpConnectionEvents(@NonNull BluetoothDevice device) {
mBrokerHandler.removeMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
device);
mBrokerHandler.removeMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION,
device);
mBrokerHandler.removeMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED,
device);
mBrokerHandler.removeMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
device);
} }
private static final class HearingAidDeviceConnectionInfo { private static final class HearingAidDeviceConnectionInfo {
@@ -428,13 +465,16 @@ import com.android.internal.annotations.GuardedBy;
sendMsgNoDelay(MSG_BROADCAST_AUDIO_BECOMING_NOISY, SENDMSG_REPLACE); sendMsgNoDelay(MSG_BROADCAST_AUDIO_BECOMING_NOISY, SENDMSG_REPLACE);
} }
/*package*/ void postA2dpSinkConnection(int state, /*package*/ void postA2dpSinkConnection(@AudioService.BtProfileConnectionState int state,
@NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) { @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) {
sendILMsg(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE, SENDMSG_QUEUE, sendILMsg(state == BluetoothA2dp.STATE_CONNECTED
? MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED
: MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
SENDMSG_QUEUE,
state, btDeviceInfo, delay); state, btDeviceInfo, delay);
} }
/*package*/ void postA2dpSourceConnection(int state, /*package*/ void postA2dpSourceConnection(@AudioService.BtProfileConnectionState int state,
@NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) { @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) {
sendILMsg(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE, sendILMsg(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE,
state, btDeviceInfo, delay); state, btDeviceInfo, delay);
@@ -520,25 +560,6 @@ import com.android.internal.annotations.GuardedBy;
} }
} }
@GuardedBy("mDeviceStateLock")
/*package*/ void handleSetA2dpSinkConnectionState(@BluetoothProfile.BtProfileState int state,
@NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) {
final int intState = (state == BluetoothA2dp.STATE_CONNECTED)
? CONNECTION_STATE_CONNECTED : CONNECTION_STATE_DISCONNECTED;
final int delay = mDeviceInventory.checkSendBecomingNoisyIntent(
AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, intState,
AudioSystem.DEVICE_NONE);
final String addr = btDeviceInfo == null ? "null" : btDeviceInfo.getBtDevice().getAddress();
if (AudioService.DEBUG_DEVICES) {
Log.d(TAG, "handleSetA2dpSinkConnectionState btDevice= " + btDeviceInfo
+ " state= " + state
+ " is dock: " + btDeviceInfo.getBtDevice().isBluetoothDock());
}
sendILMsg(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE, SENDMSG_QUEUE,
state, btDeviceInfo, delay);
}
/*package*/ void postSetA2dpSourceConnectionState(@BluetoothProfile.BtProfileState int state, /*package*/ void postSetA2dpSourceConnectionState(@BluetoothProfile.BtProfileState int state,
@NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) { @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) {
final int intState = (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0; final int intState = (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0;
@@ -573,8 +594,10 @@ import com.android.internal.annotations.GuardedBy;
// must be called synchronized on mConnectedDevices // must be called synchronized on mConnectedDevices
/*package*/ boolean hasScheduledA2dpSinkConnectionState(BluetoothDevice btDevice) { /*package*/ boolean hasScheduledA2dpSinkConnectionState(BluetoothDevice btDevice) {
return mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE, return (mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED,
new BtHelper.BluetoothA2dpDeviceInfo(btDevice)); new BtHelper.BluetoothA2dpDeviceInfo(btDevice))
|| mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
new BtHelper.BluetoothA2dpDeviceInfo(btDevice)));
} }
/*package*/ void setA2dpDockTimeout(String address, int a2dpCodec, int delayMs) { /*package*/ void setA2dpDockTimeout(String address, int a2dpCodec, int delayMs) {
@@ -700,7 +723,8 @@ import com.android.internal.annotations.GuardedBy;
mDeviceInventory.onReportNewRoutes(); mDeviceInventory.onReportNewRoutes();
} }
break; break;
case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE: case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
synchronized (mDeviceStateLock) { synchronized (mDeviceStateLock) {
mDeviceInventory.onSetA2dpSinkConnectionState( mDeviceInventory.onSetA2dpSinkConnectionState(
(BtHelper.BluetoothA2dpDeviceInfo) msg.obj, msg.arg1); (BtHelper.BluetoothA2dpDeviceInfo) msg.obj, msg.arg1);
@@ -825,7 +849,8 @@ import com.android.internal.annotations.GuardedBy;
} }
} }
break; break;
case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT: { case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION:
case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION: {
final BtDeviceConnectionInfo info = (BtDeviceConnectionInfo) msg.obj; final BtDeviceConnectionInfo info = (BtDeviceConnectionInfo) msg.obj;
AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent( AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
"setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent " "setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent "
@@ -876,7 +901,7 @@ import com.android.internal.annotations.GuardedBy;
private static final int MSG_I_BROADCAST_BT_CONNECTION_STATE = 3; private static final int MSG_I_BROADCAST_BT_CONNECTION_STATE = 3;
private static final int MSG_IIL_SET_FORCE_USE = 4; private static final int MSG_IIL_SET_FORCE_USE = 4;
private static final int MSG_IIL_SET_FORCE_BT_A2DP_USE = 5; private static final int MSG_IIL_SET_FORCE_BT_A2DP_USE = 5;
private static final int MSG_IL_SET_A2DP_SINK_CONNECTION_STATE = 6; private static final int MSG_TOGGLE_HDMI = 6;
private static final int MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE = 7; private static final int MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE = 7;
private static final int MSG_IL_SET_HEARING_AID_CONNECTION_STATE = 8; private static final int MSG_IL_SET_HEARING_AID_CONNECTION_STATE = 8;
private static final int MSG_BT_HEADSET_CNCT_FAILED = 9; private static final int MSG_BT_HEADSET_CNCT_FAILED = 9;
@@ -887,7 +912,6 @@ import com.android.internal.annotations.GuardedBy;
private static final int MSG_II_SET_HEARING_AID_VOLUME = 14; private static final int MSG_II_SET_HEARING_AID_VOLUME = 14;
private static final int MSG_I_SET_AVRCP_ABSOLUTE_VOLUME = 15; private static final int MSG_I_SET_AVRCP_ABSOLUTE_VOLUME = 15;
private static final int MSG_I_DISCONNECT_BT_SCO = 16; private static final int MSG_I_DISCONNECT_BT_SCO = 16;
private static final int MSG_TOGGLE_HDMI = 17;
private static final int MSG_L_A2DP_ACTIVE_DEVICE_CHANGE = 18; private static final int MSG_L_A2DP_ACTIVE_DEVICE_CHANGE = 18;
private static final int MSG_DISCONNECT_A2DP = 19; private static final int MSG_DISCONNECT_A2DP = 19;
private static final int MSG_DISCONNECT_A2DP_SINK = 20; private static final int MSG_DISCONNECT_A2DP_SINK = 20;
@@ -897,25 +921,30 @@ import com.android.internal.annotations.GuardedBy;
private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP_SINK = 24; private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP_SINK = 24;
private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEARING_AID = 25; private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEARING_AID = 25;
private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEADSET = 26; private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEADSET = 26;
private static final int MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED = 27;
private static final int MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED = 28;
// process external command to (dis)connect an A2DP device // process external command to (dis)connect an A2DP device
private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT = 27; private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION = 29;
private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION = 30;
// process external command to (dis)connect a hearing aid device // process external command to (dis)connect a hearing aid device
private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 28; private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 31;
// a ScoClient died in BtHelper // a ScoClient died in BtHelper
private static final int MSG_L_SCOCLIENT_DIED = 29; private static final int MSG_L_SCOCLIENT_DIED = 32;
private static boolean isMessageHandledUnderWakelock(int msgId) { private static boolean isMessageHandledUnderWakelock(int msgId) {
switch(msgId) { switch(msgId) {
case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE: case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE: case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE: case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
case MSG_IL_SET_HEARING_AID_CONNECTION_STATE: case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
case MSG_IL_BTA2DP_DOCK_TIMEOUT: case MSG_IL_BTA2DP_DOCK_TIMEOUT:
case MSG_L_A2DP_DEVICE_CONFIG_CHANGE: case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
case MSG_TOGGLE_HDMI: case MSG_TOGGLE_HDMI:
case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE: case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT: case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION:
case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION:
case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT: case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT:
return true; return true;
default: default:
@@ -996,7 +1025,8 @@ import com.android.internal.annotations.GuardedBy;
switch (msg) { switch (msg) {
case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE: case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE: case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
case MSG_IL_SET_HEARING_AID_CONNECTION_STATE: case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE: case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
case MSG_IL_BTA2DP_DOCK_TIMEOUT: case MSG_IL_BTA2DP_DOCK_TIMEOUT:

View File

@@ -41,14 +41,16 @@ import android.util.Log;
import android.util.Slog; import android.util.Slog;
import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList; import java.util.ArrayList;
/** /**
* Class to manage the inventory of all connected devices. * Class to manage the inventory of all connected devices.
* This class is thread-safe. * This class is thread-safe.
* (non final for mocking/spying)
*/ */
public final class AudioDeviceInventory { public class AudioDeviceInventory {
private static final String TAG = "AS.AudioDeviceInventory"; private static final String TAG = "AS.AudioDeviceInventory";
@@ -56,11 +58,7 @@ public final class AudioDeviceInventory {
// Key for map created from DeviceInfo.makeDeviceListKey() // Key for map created from DeviceInfo.makeDeviceListKey()
private final ArrayMap<String, DeviceInfo> mConnectedDevices = new ArrayMap<>(); private final ArrayMap<String, DeviceInfo> mConnectedDevices = new ArrayMap<>();
private final @NonNull AudioDeviceBroker mDeviceBroker; private @NonNull AudioDeviceBroker mDeviceBroker;
AudioDeviceInventory(@NonNull AudioDeviceBroker broker) {
mDeviceBroker = broker;
}
// cache of the address of the last dock the device was connected to // cache of the address of the last dock the device was connected to
private String mDockAddress; private String mDockAddress;
@@ -70,6 +68,20 @@ public final class AudioDeviceInventory {
final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers = final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers =
new RemoteCallbackList<IAudioRoutesObserver>(); new RemoteCallbackList<IAudioRoutesObserver>();
/*package*/ AudioDeviceInventory(@NonNull AudioDeviceBroker broker) {
mDeviceBroker = broker;
}
//-----------------------------------------------------------
/** for mocking only */
/*package*/ AudioDeviceInventory() {
mDeviceBroker = null;
}
/*package*/ void setDeviceBroker(@NonNull AudioDeviceBroker broker) {
mDeviceBroker = broker;
}
//------------------------------------------------------------ //------------------------------------------------------------
/** /**
* Class to store info about connected devices. * Class to store info about connected devices.
@@ -146,8 +158,10 @@ public final class AudioDeviceInventory {
} }
} }
// only public for mocking/spying
@GuardedBy("AudioDeviceBroker.mDeviceStateLock") @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, @VisibleForTesting
public void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo,
@AudioService.BtProfileConnectionState int state) { @AudioService.BtProfileConnectionState int state) {
final BluetoothDevice btDevice = btInfo.getBtDevice(); final BluetoothDevice btDevice = btInfo.getBtDevice();
int a2dpVolume = btInfo.getVolume(); int a2dpVolume = btInfo.getVolume();
@@ -159,30 +173,40 @@ public final class AudioDeviceInventory {
if (!BluetoothAdapter.checkBluetoothAddress(address)) { if (!BluetoothAdapter.checkBluetoothAddress(address)) {
address = ""; address = "";
} }
AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
"A2DP sink connected: device addr=" + address + " state=" + state
+ " vol=" + a2dpVolume));
final int a2dpCodec = btInfo.getCodec(); final int a2dpCodec = btInfo.getCodec();
AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
"A2DP sink connected: device addr=" + address + " state=" + state
+ " codec=" + a2dpCodec
+ " vol=" + a2dpVolume));
synchronized (mConnectedDevices) { synchronized (mConnectedDevices) {
final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
btDevice.getAddress()); btDevice.getAddress());
final DeviceInfo di = mConnectedDevices.get(key); final DeviceInfo di = mConnectedDevices.get(key);
boolean isConnected = di != null; boolean isConnected = di != null;
if (isConnected && state != BluetoothProfile.STATE_CONNECTED) { if (isConnected) {
if (btDevice.isBluetoothDock()) { if (state == BluetoothProfile.STATE_CONNECTED) {
if (state == BluetoothProfile.STATE_DISCONNECTED) { // device is already connected, but we are receiving a connection again,
// introduction of a delay for transient disconnections of docks when // it could be for a codec change
// power is rapidly turned off/on, this message will be canceled if if (a2dpCodec != di.mDeviceCodecFormat) {
// we reconnect the dock under a preset delay mDeviceBroker.postBluetoothA2dpDeviceConfigChange(btDevice);
makeA2dpDeviceUnavailableLater(address,
AudioDeviceBroker.BTA2DP_DOCK_TIMEOUT_MS);
// the next time isConnected is evaluated, it will be false for the dock
} }
} else { } else {
makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat); if (btDevice.isBluetoothDock()) {
if (state == BluetoothProfile.STATE_DISCONNECTED) {
// introduction of a delay for transient disconnections of docks when
// power is rapidly turned off/on, this message will be canceled if
// we reconnect the dock under a preset delay
makeA2dpDeviceUnavailableLater(address,
AudioDeviceBroker.BTA2DP_DOCK_TIMEOUT_MS);
// the next time isConnected is evaluated, it will be false for the dock
}
} else {
makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat);
}
} }
} else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) { } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
if (btDevice.isBluetoothDock()) { if (btDevice.isBluetoothDock()) {
@@ -282,11 +306,9 @@ public final class AudioDeviceInventory {
+ " event=" + BtHelper.a2dpDeviceEventToString(event))); + " event=" + BtHelper.a2dpDeviceEventToString(event)));
synchronized (mConnectedDevices) { synchronized (mConnectedDevices) {
//TODO original CL is not consistent between BluetoothDevice and BluetoothA2dpDeviceInfo
// for this type of message
if (mDeviceBroker.hasScheduledA2dpSinkConnectionState(btDevice)) { if (mDeviceBroker.hasScheduledA2dpSinkConnectionState(btDevice)) {
AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent( AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
"A2dp config change ignored")); "A2dp config change ignored (scheduled connection change)"));
return; return;
} }
final String key = DeviceInfo.makeDeviceListKey( final String key = DeviceInfo.makeDeviceListKey(
@@ -534,8 +556,10 @@ public final class AudioDeviceInventory {
return mCurAudioRoutes; return mCurAudioRoutes;
} }
// only public for mocking/spying
@GuardedBy("AudioDeviceBroker.mDeviceStateLock") @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ void setBluetoothA2dpDeviceConnectionState( @VisibleForTesting
public void setBluetoothA2dpDeviceConnectionState(
@NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state, @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
int profile, boolean suppressNoisyIntent, int musicDevice, int a2dpVolume) { int profile, boolean suppressNoisyIntent, int musicDevice, int a2dpVolume) {
int delay; int delay;
@@ -544,9 +568,12 @@ public final class AudioDeviceInventory {
} }
synchronized (mConnectedDevices) { synchronized (mConnectedDevices) {
if (profile == BluetoothProfile.A2DP && !suppressNoisyIntent) { if (profile == BluetoothProfile.A2DP && !suppressNoisyIntent) {
int intState = (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0; @AudioService.ConnectionState int asState =
(state == BluetoothA2dp.STATE_CONNECTED)
? AudioService.CONNECTION_STATE_CONNECTED
: AudioService.CONNECTION_STATE_DISCONNECTED;
delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
intState, musicDevice); asState, musicDevice);
} else { } else {
delay = 0; delay = 0;
} }
@@ -785,7 +812,7 @@ public final class AudioDeviceInventory {
return 0; return 0;
} }
mDeviceBroker.postBroadcastBecomingNoisy(); mDeviceBroker.postBroadcastBecomingNoisy();
delay = 1000; delay = AudioService.BECOMING_NOISY_DELAY_MS;
} }
return delay; return delay;
@@ -943,4 +970,21 @@ public final class AudioDeviceInventory {
intent.putExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, maxChannels); intent.putExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, maxChannels);
} }
} }
//----------------------------------------------------------
// For tests only
/**
* Check if device is in the list of connected devices
* @param device
* @return true if connected
*/
@VisibleForTesting
public boolean isA2dpDeviceConnected(@NonNull BluetoothDevice device) {
final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
device.getAddress());
synchronized (mConnectedDevices) {
return (mConnectedDevices.get(key) != null);
}
}
} }

View File

@@ -134,6 +134,7 @@ import android.view.accessibility.AccessibilityManager;
import android.widget.Toast; import android.widget.Toast;
import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils; import com.android.internal.util.DumpUtils;
import com.android.internal.util.Preconditions; import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils; import com.android.internal.util.XmlUtils;
@@ -200,6 +201,13 @@ public class AudioService extends IAudioService.Stub
/** How long to delay after a volume down event before unmuting a stream */ /** How long to delay after a volume down event before unmuting a stream */
private static final int UNMUTE_STREAM_DELAY = 350; private static final int UNMUTE_STREAM_DELAY = 350;
/**
* Delay before disconnecting a device that would cause BECOMING_NOISY intent to be sent,
* to give a chance to applications to pause.
*/
@VisibleForTesting
public static final int BECOMING_NOISY_DELAY_MS = 1000;
/** /**
* Only used in the result from {@link #checkForRingerModeChange(int, int, int)} * Only used in the result from {@link #checkForRingerModeChange(int, int, int)}
*/ */
@@ -4123,7 +4131,9 @@ public class AudioService extends IAudioService.Stub
|| adjust == AudioManager.ADJUST_TOGGLE_MUTE; || adjust == AudioManager.ADJUST_TOGGLE_MUTE;
} }
/*package*/ boolean isInCommunication() { /** only public for mocking/spying, do not call outside of AudioService */
@VisibleForTesting
public boolean isInCommunication() {
boolean IsInCall = false; boolean IsInCall = false;
TelecomManager telecomManager = TelecomManager telecomManager =
@@ -4292,7 +4302,9 @@ public class AudioService extends IAudioService.Stub
return false; return false;
} }
/*package*/ int getDeviceForStream(int stream) { /** only public for mocking/spying, do not call outside of AudioService */
@VisibleForTesting
public int getDeviceForStream(int stream) {
int device = getDevicesForStream(stream); int device = getDevicesForStream(stream);
if ((device & (device - 1)) != 0) { if ((device & (device - 1)) != 0) {
// Multiple device selection is either: // Multiple device selection is either:
@@ -4337,7 +4349,9 @@ public class AudioService extends IAudioService.Stub
} }
} }
/*package*/ void postObserveDevicesForAllStreams() { /** only public for mocking/spying, do not call outside of AudioService */
@VisibleForTesting
public void postObserveDevicesForAllStreams() {
sendMsg(mAudioHandler, sendMsg(mAudioHandler,
MSG_OBSERVE_DEVICES_FOR_ALL_STREAMS, MSG_OBSERVE_DEVICES_FOR_ALL_STREAMS,
SENDMSG_QUEUE, 0 /*arg1*/, 0 /*arg2*/, null /*obj*/, SENDMSG_QUEUE, 0 /*arg1*/, 0 /*arg2*/, null /*obj*/,
@@ -4448,7 +4462,9 @@ public class AudioService extends IAudioService.Stub
AudioSystem.DEVICE_OUT_ALL_USB | AudioSystem.DEVICE_OUT_ALL_USB |
AudioSystem.DEVICE_OUT_HDMI; AudioSystem.DEVICE_OUT_HDMI;
/*package*/ void postAccessoryPlugMediaUnmute(int newDevice) { /** only public for mocking/spying, do not call outside of AudioService */
@VisibleForTesting
public void postAccessoryPlugMediaUnmute(int newDevice) {
sendMsg(mAudioHandler, MSG_ACCESSORY_PLUG_MEDIA_UNMUTE, SENDMSG_QUEUE, sendMsg(mAudioHandler, MSG_ACCESSORY_PLUG_MEDIA_UNMUTE, SENDMSG_QUEUE,
newDevice, 0, null, 0); newDevice, 0, null, 0);
} }
@@ -4998,7 +5014,9 @@ public class AudioService extends IAudioService.Stub
} }
} }
/*package*/ void postSetVolumeIndexOnDevice(int streamType, int vssVolIndex, int device, /** only public for mocking/spying, do not call outside of AudioService */
@VisibleForTesting
public void postSetVolumeIndexOnDevice(int streamType, int vssVolIndex, int device,
String caller) { String caller) {
sendMsg(mAudioHandler, sendMsg(mAudioHandler,
MSG_SET_DEVICE_STREAM_VOLUME, MSG_SET_DEVICE_STREAM_VOLUME,
@@ -5579,7 +5597,9 @@ public class AudioService extends IAudioService.Stub
* @return true if there is currently a registered dynamic mixing policy that affects media * @return true if there is currently a registered dynamic mixing policy that affects media
* and is not a render + loopback policy * and is not a render + loopback policy
*/ */
/*package*/ boolean hasMediaDynamicPolicy() { // only public for mocking/spying
@VisibleForTesting
public boolean hasMediaDynamicPolicy() {
synchronized (mAudioPolicies) { synchronized (mAudioPolicies) {
if (mAudioPolicies.isEmpty()) { if (mAudioPolicies.isEmpty()) {
return false; return false;
@@ -5912,7 +5932,9 @@ public class AudioService extends IAudioService.Stub
return mMediaFocusControl.getFocusRampTimeMs(focusGain, attr); return mMediaFocusControl.getFocusRampTimeMs(focusGain, attr);
} }
/*package*/ boolean hasAudioFocusUsers() { /** only public for mocking/spying, do not call outside of AudioService */
@VisibleForTesting
public boolean hasAudioFocusUsers() {
return mMediaFocusControl.hasAudioFocusUsers(); return mMediaFocusControl.hasAudioFocusUsers();
} }

View File

@@ -139,6 +139,12 @@ public class BtHelper {
public int getCodec() { public int getCodec() {
return mCodec; return mCodec;
} }
// redefine equality op so we can match messages intended for this device
@Override
public boolean equals(Object o) {
return mBtDevice.equals(o);
}
} }
// A2DP device events // A2DP device events
@@ -441,9 +447,9 @@ public class BtHelper {
return; return;
} }
final BluetoothDevice btDevice = deviceList.get(0); final BluetoothDevice btDevice = deviceList.get(0);
final @BluetoothProfile.BtProfileState int state = mA2dp.getConnectionState(btDevice); // the device is guaranteed CONNECTED
mDeviceBroker.handleSetA2dpSinkConnectionState( mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(btDevice,
state, new BluetoothA2dpDeviceInfo(btDevice)); BluetoothA2dp.STATE_CONNECTED, BluetoothProfile.A2DP_SINK, true, -1);
} }
/*package*/ synchronized void onA2dpSinkProfileConnected(BluetoothProfile profile) { /*package*/ synchronized void onA2dpSinkProfileConnected(BluetoothProfile profile) {

View File

@@ -70,6 +70,8 @@
<uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.HARDWARE_TEST"/> <uses-permission android:name="android.permission.HARDWARE_TEST"/>
<uses-permission android:name="android.permission.MANAGE_APPOPS"/> <uses-permission android:name="android.permission.MANAGE_APPOPS"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<!-- Uses API introduced in O (26) --> <!-- Uses API introduced in O (26) -->
<uses-sdk android:minSdkVersion="1" <uses-sdk android:minSdkVersion="1"

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2019 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.audio;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Spy;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class AudioDeviceBrokerTest {
private static final String TAG = "AudioDeviceBrokerTest";
private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100;
private Context mContext;
// the actual class under test
private AudioDeviceBroker mAudioDeviceBroker;
@Mock private AudioService mMockAudioService;
@Spy private AudioDeviceInventory mSpyDevInventory;
private BluetoothDevice mFakeBtDevice;
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getTargetContext();
mMockAudioService = mock(AudioService.class);
mSpyDevInventory = spy(new AudioDeviceInventory());
mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory);
mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker);
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05");
Assert.assertNotNull("invalid null BT device", mFakeBtDevice);
}
@After
public void tearDown() throws Exception { }
@Test
public void testSetUpAndTearDown() { }
/**
* Verify call to postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for connection
* calls into AudioDeviceInventory with the right params
* @throws Exception
*/
@Test
public void testPostA2dpDeviceConnectionChange() throws Exception {
Log.i(TAG, "testPostA2dpDeviceConnectionChange");
Assert.assertNotNull("invalid null BT device", mFakeBtDevice);
mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 1);
Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
verify(mSpyDevInventory, times(1)).setBluetoothA2dpDeviceConnectionState(
any(BluetoothDevice.class),
ArgumentMatchers.eq(BluetoothProfile.STATE_CONNECTED) /*state*/,
ArgumentMatchers.eq(BluetoothProfile.A2DP) /*profile*/,
ArgumentMatchers.eq(true) /*suppressNoisyIntent*/, anyInt() /*musicDevice*/,
ArgumentMatchers.eq(1) /*a2dpVolume*/
);
}
/**
* Verify call to postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for
* connection > pause > disconnection > connection
* keeps the device connected
* @throws Exception
*/
@Test
public void testA2dpDeviceConnectionDisconnectionConnectionChange() throws Exception {
Log.i(TAG, "testA2dpDeviceConnectionDisconnectionConnectionChange");
doTestConnectionDisconnectionReconnection(0);
}
/**
* Verify device disconnection and reconnection within the BECOMING_NOISY window
* @throws Exception
*/
@Test
public void testA2dpDeviceReconnectionWithinBecomingNoisyDelay() throws Exception {
Log.i(TAG, "testA2dpDeviceReconnectionWithinBecomingNoisyDelay");
doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2);
}
private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection)
throws Exception {
when(mMockAudioService.getDeviceForStream(AudioManager.STREAM_MUSIC))
.thenReturn(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
when(mMockAudioService.isInCommunication()).thenReturn(false);
when(mMockAudioService.hasMediaDynamicPolicy()).thenReturn(false);
when(mMockAudioService.hasAudioFocusUsers()).thenReturn(false);
// first connection
mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 1);
Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
// disconnection
mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.A2DP, false, -1);
if (delayAfterDisconnection > 0) {
Thread.sleep(delayAfterDisconnection);
}
// reconnection
mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 2);
Thread.sleep(AudioService.BECOMING_NOISY_DELAY_MS + MAX_MESSAGE_HANDLING_DELAY_MS);
// Verify disconnection has been cancelled and we're seeing two connections attempts,
// with the device connected at the end of the test
verify(mSpyDevInventory, times(2)).onSetA2dpSinkConnectionState(
any(BtHelper.BluetoothA2dpDeviceInfo.class),
ArgumentMatchers.eq(BluetoothProfile.STATE_CONNECTED));
Assert.assertTrue("Mock device not connected",
mSpyDevInventory.isA2dpDeviceConnected(mFakeBtDevice));
}
}