diff options
author | 2019-08-01 17:51:31 +0000 | |
---|---|---|
committer | 2019-08-01 17:51:31 +0000 | |
commit | 0cfb40f2dea94f0e1c99a19af47b4aea53b07804 (patch) | |
tree | 0b182d344114ef11f792552a0b32fc33b8941918 | |
parent | 165f56a092fdd101adbba205dc9e4b7a72f5963d (diff) | |
parent | dc552e9d88ca8e65baede28f33497a5dfd013185 (diff) |
Merge "AudioService: fix disconnect/connect of A2DP device"
6 files changed, 343 insertions, 78 deletions
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index cb6cf74d4f52..6010b1dc88c4 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -15,9 +15,6 @@ */ 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.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothDevice; @@ -95,13 +92,28 @@ import java.io.PrintWriter; /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service) { mContext = context; mAudioService = service; - setupMessaging(context); mBtHelper = new BtHelper(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; mForcedUseForCommExt = mForcedUseForComm; - } /*package*/ Context getContext() { @@ -232,17 +244,42 @@ import java.io.PrintWriter; mSupprNoisy = suppressNoisyIntent; 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( @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state, int profile, boolean suppressNoisyIntent, int a2dpVolume) { final BtDeviceConnectionInfo info = new BtDeviceConnectionInfo(device, state, profile, suppressNoisyIntent, a2dpVolume); - // TODO add a check to try to remove unprocessed messages for the same device (the old - // check didn't work), and make sure it doesn't conflict with config change message - sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info); + // when receiving a request to change the connection state of a device, this last request + // is the source of truth, so cancel all previous requests + 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 { @@ -430,13 +467,16 @@ import java.io.PrintWriter; 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) { - 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); } - /*package*/ void postA2dpSourceConnection(int state, + /*package*/ void postA2dpSourceConnection(@AudioService.BtProfileConnectionState int state, @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) { sendILMsg(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE, state, btDeviceInfo, delay); @@ -522,25 +562,6 @@ import java.io.PrintWriter; } } - @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, @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) { final int intState = (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0; @@ -575,8 +596,10 @@ import java.io.PrintWriter; // must be called synchronized on mConnectedDevices /*package*/ boolean hasScheduledA2dpSinkConnectionState(BluetoothDevice btDevice) { - return mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE, - new BtHelper.BluetoothA2dpDeviceInfo(btDevice)); + return (mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED, + 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) { @@ -711,7 +734,8 @@ import java.io.PrintWriter; mDeviceInventory.onReportNewRoutes(); } 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) { mDeviceInventory.onSetA2dpSinkConnectionState( (BtHelper.BluetoothA2dpDeviceInfo) msg.obj, msg.arg1); @@ -836,7 +860,8 @@ import java.io.PrintWriter; } } 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; AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent( "setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent " @@ -887,7 +912,7 @@ import java.io.PrintWriter; 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_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_HEARING_AID_CONNECTION_STATE = 8; private static final int MSG_BT_HEADSET_CNCT_FAILED = 9; @@ -898,7 +923,6 @@ import java.io.PrintWriter; 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_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_DISCONNECT_A2DP = 19; private static final int MSG_DISCONNECT_A2DP_SINK = 20; @@ -908,25 +932,30 @@ import java.io.PrintWriter; 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_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 - 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 - 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 - private static final int MSG_L_SCOCLIENT_DIED = 29; + private static final int MSG_L_SCOCLIENT_DIED = 32; private static boolean isMessageHandledUnderWakelock(int msgId) { switch(msgId) { 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_HEARING_AID_CONNECTION_STATE: case MSG_IL_BTA2DP_DOCK_TIMEOUT: case MSG_L_A2DP_DEVICE_CONFIG_CHANGE: case MSG_TOGGLE_HDMI: 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: return true; default: @@ -1007,7 +1036,8 @@ import java.io.PrintWriter; switch (msg) { 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_L_SET_WIRED_DEVICE_CONNECTION_STATE: case MSG_IL_BTA2DP_DOCK_TIMEOUT: diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index a9a8ef2f7e12..90973a888a9d 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -41,14 +41,16 @@ import android.util.Log; import android.util.Slog; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; /** * Class to manage the inventory of all connected devices. * This class is thread-safe. + * (non final for mocking/spying) */ -public final class AudioDeviceInventory { +public class AudioDeviceInventory { private static final String TAG = "AS.AudioDeviceInventory"; @@ -56,11 +58,7 @@ public final class AudioDeviceInventory { // Key for map created from DeviceInfo.makeDeviceListKey() private final ArrayMap<String, DeviceInfo> mConnectedDevices = new ArrayMap<>(); - private final @NonNull AudioDeviceBroker mDeviceBroker; - - AudioDeviceInventory(@NonNull AudioDeviceBroker broker) { - mDeviceBroker = broker; - } + private @NonNull AudioDeviceBroker mDeviceBroker; // cache of the address of the last dock the device was connected to private String mDockAddress; @@ -70,6 +68,20 @@ public final class AudioDeviceInventory { final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers = 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. @@ -146,8 +158,10 @@ public final class AudioDeviceInventory { } } + // only public for mocking/spying @GuardedBy("AudioDeviceBroker.mDeviceStateLock") - /*package*/ void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, + @VisibleForTesting + public void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, @AudioService.BtProfileConnectionState int state) { final BluetoothDevice btDevice = btInfo.getBtDevice(); int a2dpVolume = btInfo.getVolume(); @@ -159,30 +173,40 @@ public final class AudioDeviceInventory { if (!BluetoothAdapter.checkBluetoothAddress(address)) { address = ""; } + + final int a2dpCodec = btInfo.getCodec(); + AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent( "A2DP sink connected: device addr=" + address + " state=" + state + + " codec=" + a2dpCodec + " vol=" + a2dpVolume)); - final int a2dpCodec = btInfo.getCodec(); - synchronized (mConnectedDevices) { final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, btDevice.getAddress()); final DeviceInfo di = mConnectedDevices.get(key); boolean isConnected = di != null; - if (isConnected && state != BluetoothProfile.STATE_CONNECTED) { - 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 + if (isConnected) { + if (state == BluetoothProfile.STATE_CONNECTED) { + // device is already connected, but we are receiving a connection again, + // it could be for a codec change + if (a2dpCodec != di.mDeviceCodecFormat) { + mDeviceBroker.postBluetoothA2dpDeviceConfigChange(btDevice); } } 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) { if (btDevice.isBluetoothDock()) { @@ -282,11 +306,9 @@ public final class AudioDeviceInventory { + " event=" + BtHelper.a2dpDeviceEventToString(event))); synchronized (mConnectedDevices) { - //TODO original CL is not consistent between BluetoothDevice and BluetoothA2dpDeviceInfo - // for this type of message if (mDeviceBroker.hasScheduledA2dpSinkConnectionState(btDevice)) { AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent( - "A2dp config change ignored")); + "A2dp config change ignored (scheduled connection change)")); return; } final String key = DeviceInfo.makeDeviceListKey( @@ -534,8 +556,10 @@ public final class AudioDeviceInventory { return mCurAudioRoutes; } + // only public for mocking/spying @GuardedBy("AudioDeviceBroker.mDeviceStateLock") - /*package*/ void setBluetoothA2dpDeviceConnectionState( + @VisibleForTesting + public void setBluetoothA2dpDeviceConnectionState( @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state, int profile, boolean suppressNoisyIntent, int musicDevice, int a2dpVolume) { int delay; @@ -544,9 +568,12 @@ public final class AudioDeviceInventory { } synchronized (mConnectedDevices) { 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, - intState, musicDevice); + asState, musicDevice); } else { delay = 0; } @@ -785,7 +812,7 @@ public final class AudioDeviceInventory { return 0; } mDeviceBroker.postBroadcastBecomingNoisy(); - delay = 1000; + delay = AudioService.BECOMING_NOISY_DELAY_MS; } return delay; @@ -943,4 +970,21 @@ public final class AudioDeviceInventory { 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); + } + } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 7458bee793db..5bc2261878b6 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -128,6 +128,7 @@ import android.view.accessibility.AccessibilityManager; import android.widget.Toast; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; import com.android.server.EventLogTags; @@ -190,6 +191,13 @@ public class AudioService extends IAudioService.Stub 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)} */ private static final int FLAG_ADJUST_VOLUME = 1; @@ -3950,7 +3958,9 @@ public class AudioService extends IAudioService.Stub || 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; TelecomManager telecomManager = @@ -4119,7 +4129,9 @@ public class AudioService extends IAudioService.Stub 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); if ((device & (device - 1)) != 0) { // Multiple device selection is either: @@ -4164,7 +4176,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, MSG_OBSERVE_DEVICES_FOR_ALL_STREAMS, SENDMSG_QUEUE, 0 /*arg1*/, 0 /*arg2*/, null /*obj*/, @@ -4275,7 +4289,9 @@ public class AudioService extends IAudioService.Stub AudioSystem.DEVICE_OUT_ALL_USB | 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, newDevice, 0, null, 0); } @@ -4825,7 +4841,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) { sendMsg(mAudioHandler, MSG_SET_DEVICE_STREAM_VOLUME, @@ -5183,7 +5201,9 @@ public class AudioService extends IAudioService.Stub * @return true if there is currently a registered dynamic mixing policy that affects media * and is not a render + loopback policy */ - /*package*/ boolean hasMediaDynamicPolicy() { + // only public for mocking/spying + @VisibleForTesting + public boolean hasMediaDynamicPolicy() { synchronized (mAudioPolicies) { if (mAudioPolicies.isEmpty()) { return false; @@ -5516,7 +5536,9 @@ public class AudioService extends IAudioService.Stub 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(); } diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 1a63f8f51ee3..9f1a6bd15ac3 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -139,6 +139,12 @@ public class BtHelper { public int getCodec() { 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 @@ -441,9 +447,9 @@ public class BtHelper { return; } final BluetoothDevice btDevice = deviceList.get(0); - final @BluetoothProfile.BtProfileState int state = mA2dp.getConnectionState(btDevice); - mDeviceBroker.handleSetA2dpSinkConnectionState( - state, new BluetoothA2dpDeviceInfo(btDevice)); + // the device is guaranteed CONNECTED + mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(btDevice, + BluetoothA2dp.STATE_CONNECTED, BluetoothProfile.A2DP_SINK, true, -1); } /*package*/ synchronized void onA2dpSinkProfileConnected(BluetoothProfile profile) { diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 01f2f6b26415..c1bbb307c9f5 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -69,6 +69,7 @@ <uses-permission android:name="android.permission.READ_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.BLUETOOTH"/> <!-- Uses API introduced in O (26) --> <uses-sdk android:minSdkVersion="1" diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java new file mode 100644 index 000000000000..5c2ad94720f0 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java @@ -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)); + } +} |