[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:
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user