1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.server.telecom.tests; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.mockito.ArgumentMatchers.anyBoolean; 21 import static org.mockito.ArgumentMatchers.anyString; 22 import static org.mockito.ArgumentMatchers.eq; 23 import static org.mockito.ArgumentMatchers.nullable; 24 import static org.mockito.Mockito.never; 25 import static org.mockito.Mockito.reset; 26 import static org.mockito.Mockito.times; 27 import static org.mockito.Mockito.verify; 28 import static org.mockito.Mockito.when; 29 30 import android.bluetooth.BluetoothAdapter; 31 import android.bluetooth.BluetoothDevice; 32 import android.bluetooth.BluetoothHeadset; 33 import android.bluetooth.BluetoothHearingAid; 34 import android.bluetooth.BluetoothLeAudio; 35 import android.bluetooth.BluetoothProfile; 36 import android.bluetooth.BluetoothStatusCodes; 37 import android.content.ContentResolver; 38 import android.media.AudioDeviceInfo; 39 import android.os.Parcel; 40 import android.telecom.Log; 41 42 import androidx.test.filters.SmallTest; 43 44 import com.android.internal.os.SomeArgs; 45 import com.android.server.telecom.CallAudioCommunicationDeviceTracker; 46 import com.android.server.telecom.TelecomSystem; 47 import com.android.server.telecom.Timeouts; 48 import com.android.server.telecom.bluetooth.BluetoothDeviceManager; 49 import com.android.server.telecom.bluetooth.BluetoothRouteManager; 50 51 import org.junit.After; 52 import org.junit.Before; 53 import org.junit.Test; 54 import org.junit.runner.RunWith; 55 import org.junit.runners.JUnit4; 56 import org.mockito.Mock; 57 58 import java.util.Arrays; 59 import java.util.List; 60 import java.util.stream.Collectors; 61 import java.util.stream.Stream; 62 63 @RunWith(JUnit4.class) 64 public class BluetoothRouteManagerTest extends TelecomTestCase { 65 private static final int TEST_TIMEOUT = 1000; 66 static final BluetoothDevice DEVICE1 = makeBluetoothDevice("00:00:00:00:00:01"); 67 static final BluetoothDevice DEVICE2 = makeBluetoothDevice("00:00:00:00:00:02"); 68 static final BluetoothDevice DEVICE3 = makeBluetoothDevice("00:00:00:00:00:03"); 69 static final BluetoothDevice HEARING_AID_DEVICE_LEFT = makeBluetoothDevice("CA:FE:DE:CA:00:01"); 70 static final BluetoothDevice HEARING_AID_DEVICE_RIGHT = 71 makeBluetoothDevice("CA:FE:DE:CA:00:02"); 72 // See HearingAidService#getActiveDevices 73 // Note: It is really important that the left HA is the first one. The left HA is always 74 // in the first index (0) and the right one in the second index (1). 75 static final BluetoothDevice[] HEARING_AIDS = 76 new BluetoothDevice[]{HEARING_AID_DEVICE_LEFT, HEARING_AID_DEVICE_RIGHT}; 77 78 @Mock private BluetoothAdapter mBluetoothAdapter; 79 @Mock private BluetoothDeviceManager mDeviceManager; 80 @Mock private BluetoothHeadset mBluetoothHeadset; 81 @Mock private BluetoothHearingAid mBluetoothHearingAid; 82 @Mock private BluetoothLeAudio mBluetoothLeAudio; 83 @Mock private Timeouts.Adapter mTimeoutsAdapter; 84 @Mock private BluetoothRouteManager.BluetoothStateListener mListener; 85 @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; 86 87 @Override 88 @Before setUp()89 public void setUp() throws Exception { 90 super.setUp(); 91 } 92 93 @Override 94 @After tearDown()95 public void tearDown() throws Exception { 96 super.tearDown(); 97 } 98 99 @SmallTest 100 @Test testConnectLeftHearingAidWhenLeftIsActive()101 public void testConnectLeftHearingAidWhenLeftIsActive() { 102 BluetoothRouteManager sm = setupStateMachine( 103 BluetoothRouteManager.AUDIO_OFF_STATE_NAME, HEARING_AID_DEVICE_LEFT); 104 sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT, 105 BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID); 106 when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true); 107 when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true); 108 when(mCommunicationDeviceTracker.isAudioDeviceSetForType( 109 eq(AudioDeviceInfo.TYPE_HEARING_AID))).thenReturn(true); 110 111 setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null); 112 when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class))) 113 .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED); 114 115 executeRoutingAction(sm, 116 BluetoothRouteManager.NEW_DEVICE_CONNECTED, HEARING_AID_DEVICE_LEFT.getAddress()); 117 118 executeRoutingAction(sm, 119 BluetoothRouteManager.CONNECT_BT, HEARING_AID_DEVICE_LEFT.getAddress()); 120 121 assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX 122 + ":" + HEARING_AID_DEVICE_LEFT.getAddress(), sm.getCurrentState().getName()); 123 124 sm.quitNow(); 125 } 126 127 @SmallTest 128 @Test testConnectRightHearingAidWhenLeftIsActive()129 public void testConnectRightHearingAidWhenLeftIsActive() { 130 BluetoothRouteManager sm = setupStateMachine( 131 BluetoothRouteManager.AUDIO_OFF_STATE_NAME, HEARING_AID_DEVICE_RIGHT); 132 sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT, 133 BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID); 134 when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true); 135 when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true); 136 when(mCommunicationDeviceTracker.isAudioDeviceSetForType( 137 eq(AudioDeviceInfo.TYPE_HEARING_AID))).thenReturn(true); 138 139 setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null); 140 when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class))) 141 .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED); 142 143 executeRoutingAction(sm, 144 BluetoothRouteManager.NEW_DEVICE_CONNECTED, HEARING_AID_DEVICE_LEFT.getAddress()); 145 146 executeRoutingAction(sm, 147 BluetoothRouteManager.CONNECT_BT, HEARING_AID_DEVICE_LEFT.getAddress()); 148 149 assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX 150 + ":" + HEARING_AID_DEVICE_LEFT.getAddress(), sm.getCurrentState().getName()); 151 152 sm.quitNow(); 153 } 154 155 @SmallTest 156 @Test testConnectBtRetryWhileNotConnected()157 public void testConnectBtRetryWhileNotConnected() { 158 BluetoothRouteManager sm = setupStateMachine( 159 BluetoothRouteManager.AUDIO_OFF_STATE_NAME, null); 160 setupConnectedDevices(new BluetoothDevice[]{DEVICE1}, null, null, null, null, null); 161 when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis( 162 nullable(ContentResolver.class))).thenReturn(0L); 163 when(mBluetoothHeadset.connectAudio()).thenReturn(BluetoothStatusCodes.ERROR_UNKNOWN); 164 executeRoutingAction(sm, BluetoothRouteManager.CONNECT_BT, DEVICE1.getAddress()); 165 // Wait 3 times: for the first connection attempt, the retry attempt, 166 // the second retry, and once more to make sure there are only three attempts. 167 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 168 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 169 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 170 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 171 verifyConnectionAttempt(DEVICE1, 3); 172 assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME, sm.getCurrentState().getName()); 173 sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT); 174 sm.quitNow(); 175 } 176 177 @SmallTest 178 @Test testAmbiguousActiveDevice()179 public void testAmbiguousActiveDevice() { 180 BluetoothRouteManager sm = setupStateMachine( 181 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1); 182 setupConnectedDevices(new BluetoothDevice[]{DEVICE1}, 183 HEARING_AIDS, new BluetoothDevice[]{DEVICE2}, 184 DEVICE1, HEARING_AIDS, DEVICE2); 185 sm.onActiveDeviceChanged(DEVICE1, BluetoothDeviceManager.DEVICE_TYPE_HEADSET); 186 sm.onActiveDeviceChanged(DEVICE2, BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO); 187 sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT, 188 BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID); 189 executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST, DEVICE1.getAddress()); 190 191 verifyConnectionAttempt(HEARING_AID_DEVICE_LEFT, 0); 192 verifyConnectionAttempt(DEVICE1, 0); 193 verifyConnectionAttempt(DEVICE2, 0); 194 assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX 195 + ":" + DEVICE1.getAddress(), 196 sm.getCurrentState().getName()); 197 sm.quitNow(); 198 } 199 200 @SmallTest 201 @Test testAudioOnDeviceWithScoOffActiveDevice()202 public void testAudioOnDeviceWithScoOffActiveDevice() { 203 BluetoothRouteManager sm = setupStateMachine( 204 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1); 205 setupConnectedDevices(new BluetoothDevice[]{DEVICE1}, null, null, DEVICE1, null, null); 206 when(mBluetoothHeadset.getAudioState(DEVICE1)) 207 .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED); 208 executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST, DEVICE1.getAddress()); 209 210 verifyConnectionAttempt(DEVICE1, 0); 211 assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME, 212 sm.getCurrentState().getName()); 213 sm.quitNow(); 214 } 215 216 @SmallTest 217 @Test testConnectBtRetryWhileConnectedToAnotherDevice()218 public void testConnectBtRetryWhileConnectedToAnotherDevice() { 219 BluetoothRouteManager sm = setupStateMachine( 220 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1); 221 setupConnectedDevices(new BluetoothDevice[]{DEVICE1, DEVICE2}, null, null, null, null, 222 null); 223 when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis( 224 nullable(ContentResolver.class))).thenReturn(0L); 225 when(mBluetoothHeadset.connectAudio()).thenReturn(BluetoothStatusCodes.ERROR_UNKNOWN); 226 executeRoutingAction(sm, BluetoothRouteManager.CONNECT_BT, DEVICE2.getAddress()); 227 // Wait 3 times: the first connection attempt is accounted for in executeRoutingAction, 228 // so wait twice for the retry attempt, again to make sure there are only three attempts, 229 // and once more for good luck. 230 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 231 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 232 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 233 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 234 verifyConnectionAttempt(DEVICE2, 3); 235 assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX 236 + ":" + DEVICE1.getAddress(), 237 sm.getCurrentState().getName()); 238 sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT); 239 sm.quitNow(); 240 } 241 242 @SmallTest 243 @Test testSkipInactiveBtDeviceWhenEvaluateActualState()244 public void testSkipInactiveBtDeviceWhenEvaluateActualState() { 245 BluetoothRouteManager sm = setupStateMachine( 246 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, HEARING_AID_DEVICE_LEFT); 247 setupConnectedDevices(null, HEARING_AIDS, 248 null, null, HEARING_AIDS, null); 249 executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST, 250 HEARING_AID_DEVICE_LEFT.getAddress()); 251 assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME, sm.getCurrentState().getName()); 252 sm.quitNow(); 253 } 254 255 @SmallTest 256 @Test testConnectBtWithoutAddress_SwitchingBtDeviceFlag()257 public void testConnectBtWithoutAddress_SwitchingBtDeviceFlag() { 258 when(mFeatureFlags.resolveSwitchingBtDevicesComputation()).thenReturn(true); 259 verifyConnectBtWithoutAddress(); 260 } 261 262 @SmallTest 263 @Test testConnectBtWithoutAddress_SwitchingBtDeviceFlagDisabled()264 public void testConnectBtWithoutAddress_SwitchingBtDeviceFlagDisabled() { 265 verifyConnectBtWithoutAddress(); 266 } 267 verifyConnectBtWithoutAddress()268 private void verifyConnectBtWithoutAddress() { 269 when(mFeatureFlags.useActualAddressToEnterConnectingState()).thenReturn(true); 270 BluetoothRouteManager sm = setupStateMachine( 271 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1); 272 setupConnectedDevices(new BluetoothDevice[]{DEVICE1, DEVICE2}, null, null, null, null, 273 null); 274 when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis( 275 nullable(ContentResolver.class))).thenReturn(0L); 276 when(mBluetoothHeadset.connectAudio()).thenReturn(BluetoothStatusCodes.ERROR_UNKNOWN); 277 executeRoutingAction(sm, BluetoothRouteManager.CONNECT_BT, null); 278 // Wait 3 times: the first connection attempt is accounted for in executeRoutingAction, 279 // so wait twice for the retry attempt, again to make sure there are only three attempts, 280 // and once more for good luck. 281 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 282 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 283 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 284 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 285 // We should not expect explicit connection attempt (BluetoothDeviceManager#connectAudio) 286 // as the device is already "connected" as per how the state machine was initialized. 287 if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) { 288 verify(mDeviceManager, never()).disconnectAudio(); 289 } else { 290 // Legacy behavior 291 verifyConnectionAttempt(DEVICE1, 1); 292 verify(mDeviceManager, times(1)).disconnectAudio(); 293 } 294 assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX 295 + ":" + DEVICE1.getAddress(), 296 sm.getCurrentState().getName()); 297 sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT); 298 sm.quitNow(); 299 } 300 setupStateMachine(String initialState, BluetoothDevice initialDevice)301 private BluetoothRouteManager setupStateMachine(String initialState, 302 BluetoothDevice initialDevice) { 303 resetMocks(); 304 BluetoothRouteManager sm = new BluetoothRouteManager(mContext, 305 new TelecomSystem.SyncRoot() { }, mDeviceManager, 306 mTimeoutsAdapter, mCommunicationDeviceTracker, mFeatureFlags, 307 mContext.getMainLooper()); 308 sm.setListener(mListener); 309 sm.setInitialStateForTesting(initialState, initialDevice); 310 waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); 311 resetMocks(); 312 return sm; 313 } 314 setupConnectedDevices(BluetoothDevice[] hfpDevices, BluetoothDevice[] hearingAidDevices, BluetoothDevice[] leAudioDevices, BluetoothDevice hfpActiveDevice, BluetoothDevice[] hearingAidActiveDevices, BluetoothDevice leAudioDevice)315 private void setupConnectedDevices(BluetoothDevice[] hfpDevices, 316 BluetoothDevice[] hearingAidDevices, BluetoothDevice[] leAudioDevices, 317 BluetoothDevice hfpActiveDevice, BluetoothDevice[] hearingAidActiveDevices, 318 BluetoothDevice leAudioDevice) { 319 if (hfpDevices == null) hfpDevices = new BluetoothDevice[]{}; 320 if (hearingAidDevices == null) hearingAidDevices = new BluetoothDevice[]{}; 321 if (hearingAidActiveDevices == null) hearingAidActiveDevices = new BluetoothDevice[]{}; 322 if (leAudioDevice == null) leAudioDevices = new BluetoothDevice[]{}; 323 324 when(mDeviceManager.getNumConnectedDevices()).thenReturn( 325 hfpDevices.length + hearingAidDevices.length + leAudioDevices.length); 326 List<BluetoothDevice> allDevices = Stream.of( 327 Arrays.stream(hfpDevices), Arrays.stream(hearingAidDevices), 328 Arrays.stream(leAudioDevices)).flatMap(i -> i).collect(Collectors.toList()); 329 330 when(mDeviceManager.getConnectedDevices()).thenReturn(allDevices); 331 when(mBluetoothHeadset.getConnectedDevices()).thenReturn(Arrays.asList(hfpDevices)); 332 when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.HEADSET))) 333 .thenReturn(Arrays.asList(hfpActiveDevice)); 334 when(mBluetoothHeadset.getAudioState(hfpActiveDevice)) 335 .thenReturn(BluetoothHeadset.STATE_AUDIO_CONNECTED); 336 337 when(mBluetoothHearingAid.getConnectedDevices()) 338 .thenReturn(Arrays.asList(hearingAidDevices)); 339 when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.HEARING_AID))) 340 .thenReturn(Arrays.asList(hearingAidActiveDevices)); 341 when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.LE_AUDIO))) 342 .thenReturn(Arrays.asList(leAudioDevice, null)); 343 } 344 executeRoutingAction(BluetoothRouteManager brm, int message, String device)345 static void executeRoutingAction(BluetoothRouteManager brm, int message, String 346 device) { 347 SomeArgs args = SomeArgs.obtain(); 348 args.arg1 = Log.createSubsession(); 349 args.arg2 = device; 350 brm.sendMessage(message, args); 351 waitForHandlerAction(brm.getHandler(), TEST_TIMEOUT); 352 } 353 makeBluetoothDevice(String address)354 public static BluetoothDevice makeBluetoothDevice(String address) { 355 Parcel p1 = Parcel.obtain(); 356 p1.writeString(address); 357 p1.setDataPosition(0); 358 BluetoothDevice device = BluetoothDevice.CREATOR.createFromParcel(p1); 359 p1.recycle(); 360 return device; 361 } 362 resetMocks()363 private void resetMocks() { 364 reset(mDeviceManager, mListener, mBluetoothHeadset, mTimeoutsAdapter); 365 when(mDeviceManager.getBluetoothHeadset()).thenReturn(mBluetoothHeadset); 366 when(mDeviceManager.getBluetoothHearingAid()).thenReturn(mBluetoothHearingAid); 367 when(mDeviceManager.getBluetoothAdapter()).thenReturn(mBluetoothAdapter); 368 when(mDeviceManager.getLeAudioService()).thenReturn(mBluetoothLeAudio); 369 when(mBluetoothHeadset.connectAudio()).thenReturn(BluetoothStatusCodes.SUCCESS); 370 when(mBluetoothAdapter.setActiveDevice(nullable(BluetoothDevice.class), 371 eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true); 372 when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis( 373 nullable(ContentResolver.class))).thenReturn(100000L); 374 when(mTimeoutsAdapter.getBluetoothPendingTimeoutMillis( 375 nullable(ContentResolver.class))).thenReturn(100000L); 376 } 377 verifyConnectionAttempt(BluetoothDevice device, int numTimes)378 private void verifyConnectionAttempt(BluetoothDevice device, int numTimes) { 379 verify(mDeviceManager, times(numTimes)).connectAudio(eq(device.getAddress()), 380 anyBoolean()); 381 } 382 } 383