1/* 2 * Copyright (C) 2018 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 17package com.android.settings.sound; 18 19import static android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_SCO; 20import static android.media.AudioSystem.DEVICE_OUT_HEARING_AID; 21import static android.media.AudioSystem.DEVICE_OUT_USB_HEADSET; 22 23import static com.google.common.truth.Truth.assertThat; 24 25import static org.mockito.ArgumentMatchers.any; 26import static org.mockito.Mockito.mock; 27import static org.mockito.Mockito.spy; 28import static org.mockito.Mockito.times; 29import static org.mockito.Mockito.verify; 30import static org.mockito.Mockito.when; 31 32import android.bluetooth.BluetoothAdapter; 33import android.bluetooth.BluetoothDevice; 34import android.bluetooth.BluetoothManager; 35import android.content.Context; 36import android.media.AudioManager; 37import android.support.v7.preference.ListPreference; 38import android.support.v7.preference.PreferenceManager; 39import android.support.v7.preference.PreferenceScreen; 40 41import com.android.settings.R; 42import com.android.settings.testutils.SettingsRobolectricTestRunner; 43import com.android.settings.testutils.shadow.ShadowAudioManager; 44import com.android.settings.testutils.shadow.ShadowBluetoothUtils; 45import com.android.settings.testutils.shadow.ShadowMediaRouter; 46import com.android.settingslib.bluetooth.BluetoothEventManager; 47import com.android.settingslib.bluetooth.HeadsetProfile; 48import com.android.settingslib.bluetooth.HearingAidProfile; 49import com.android.settingslib.bluetooth.LocalBluetoothManager; 50import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 51 52import org.junit.After; 53import org.junit.Before; 54import org.junit.Test; 55import org.junit.runner.RunWith; 56import org.mockito.Mock; 57import org.mockito.MockitoAnnotations; 58import org.robolectric.RuntimeEnvironment; 59import org.robolectric.annotation.Config; 60import org.robolectric.shadows.ShadowBluetoothDevice; 61 62import java.util.ArrayList; 63import java.util.List; 64 65@RunWith(SettingsRobolectricTestRunner.class) 66@Config(shadows = { 67 ShadowAudioManager.class, 68 ShadowMediaRouter.class, 69 ShadowBluetoothUtils.class, 70 ShadowBluetoothDevice.class} 71) 72public class HandsFreeProfileOutputPreferenceControllerTest { 73 private static final String TEST_KEY = "Test_Key"; 74 private static final String TEST_DEVICE_NAME_1 = "Test_HFP_BT_Device_NAME_1"; 75 private static final String TEST_DEVICE_NAME_2 = "Test_HFP_BT_Device_NAME_2"; 76 private static final String TEST_HAP_DEVICE_NAME_1 = "Test_HAP_BT_Device_NAME_1"; 77 private static final String TEST_HAP_DEVICE_NAME_2 = "Test_HAP_BT_Device_NAME_2"; 78 private static final String TEST_DEVICE_ADDRESS_1 = "00:A1:A1:A1:A1:A1"; 79 private static final String TEST_DEVICE_ADDRESS_2 = "00:B2:B2:B2:B2:B2"; 80 private static final String TEST_DEVICE_ADDRESS_3 = "00:C3:C3:C3:C3:C3"; 81 private static final String TEST_DEVICE_ADDRESS_4 = "00:D4:D4:D4:D4:D4"; 82 private final static long HISYNCID1 = 10; 83 private final static long HISYNCID2 = 11; 84 85 @Mock 86 private LocalBluetoothManager mLocalManager; 87 @Mock 88 private BluetoothEventManager mBluetoothEventManager; 89 @Mock 90 private LocalBluetoothProfileManager mLocalBluetoothProfileManager; 91 @Mock 92 private HeadsetProfile mHeadsetProfile; 93 @Mock 94 private HearingAidProfile mHearingAidProfile; 95 @Mock 96 private AudioSwitchPreferenceController.AudioSwitchCallback mAudioSwitchPreferenceCallback; 97 98 private Context mContext; 99 private PreferenceScreen mScreen; 100 private ListPreference mPreference; 101 private ShadowAudioManager mShadowAudioManager; 102 private ShadowMediaRouter mShadowMediaRouter; 103 private BluetoothManager mBluetoothManager; 104 private BluetoothAdapter mBluetoothAdapter; 105 private BluetoothDevice mBluetoothDevice; 106 private BluetoothDevice mSecondBluetoothDevice; 107 private BluetoothDevice mLeftBluetoothHapDevice; 108 private BluetoothDevice mRightBluetoothHapDevice; 109 private LocalBluetoothManager mLocalBluetoothManager; 110 private AudioSwitchPreferenceController mController; 111 private List<BluetoothDevice> mProfileConnectedDevices; 112 private List<BluetoothDevice> mHearingAidActiveDevices; 113 114 @Before 115 public void setUp() { 116 MockitoAnnotations.initMocks(this); 117 mContext = spy(RuntimeEnvironment.application); 118 119 mShadowAudioManager = ShadowAudioManager.getShadow(); 120 mShadowMediaRouter = ShadowMediaRouter.getShadow(); 121 122 ShadowBluetoothUtils.sLocalBluetoothManager = mLocalManager; 123 mLocalBluetoothManager = ShadowBluetoothUtils.getLocalBtManager(mContext); 124 125 when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); 126 when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); 127 when(mLocalBluetoothProfileManager.getHeadsetProfile()).thenReturn(mHeadsetProfile); 128 when(mLocalBluetoothProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile); 129 130 mBluetoothManager = new BluetoothManager(mContext); 131 mBluetoothAdapter = mBluetoothManager.getAdapter(); 132 133 mBluetoothDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_1)); 134 when(mBluetoothDevice.getName()).thenReturn(TEST_DEVICE_NAME_1); 135 when(mBluetoothDevice.isConnected()).thenReturn(true); 136 137 mSecondBluetoothDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_2)); 138 when(mSecondBluetoothDevice.getName()).thenReturn(TEST_DEVICE_NAME_2); 139 when(mSecondBluetoothDevice.isConnected()).thenReturn(true); 140 141 mLeftBluetoothHapDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_3)); 142 when(mLeftBluetoothHapDevice.getName()).thenReturn(TEST_HAP_DEVICE_NAME_1); 143 when(mLeftBluetoothHapDevice.isConnected()).thenReturn(true); 144 145 mRightBluetoothHapDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_4)); 146 when(mRightBluetoothHapDevice.getName()).thenReturn(TEST_HAP_DEVICE_NAME_2); 147 when(mRightBluetoothHapDevice.isConnected()).thenReturn(true); 148 149 mController = new HandsFreeProfileOutputPreferenceController(mContext, TEST_KEY); 150 mScreen = spy(new PreferenceScreen(mContext, null)); 151 mPreference = new ListPreference(mContext); 152 mProfileConnectedDevices = new ArrayList<>(); 153 mHearingAidActiveDevices = new ArrayList<>(2); 154 155 when(mScreen.getPreferenceManager()).thenReturn(mock(PreferenceManager.class)); 156 when(mScreen.getContext()).thenReturn(mContext); 157 when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference); 158 mScreen.addPreference(mPreference); 159 mController.displayPreference(mScreen); 160 mController.setCallback(mAudioSwitchPreferenceCallback); 161 } 162 163 @After 164 public void tearDown() { 165 mShadowAudioManager.reset(); 166 mShadowMediaRouter.reset(); 167 ShadowBluetoothUtils.reset(); 168 } 169 170 /** 171 * During a call, bluetooth device with HisyncId. 172 * HearingAidProfile should set active device to this device. 173 */ 174 @Test 175 public void setActiveBluetoothDevice_btDeviceWithHisyncId_shouldSetBtDeviceActive() { 176 mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 177 when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1); 178 179 mController.setActiveBluetoothDevice(mLeftBluetoothHapDevice); 180 181 verify(mHearingAidProfile).setActiveDevice(mLeftBluetoothHapDevice); 182 } 183 184 /** 185 * During a call, Bluetooth device without HisyncId. 186 * HeadsetProfile should set active device to this device. 187 */ 188 @Test 189 public void setActiveBluetoothDevice_btDeviceWithoutHisyncId_shouldSetBtDeviceActive() { 190 mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 191 192 mController.setActiveBluetoothDevice(mBluetoothDevice); 193 194 verify(mHeadsetProfile).setActiveDevice(mBluetoothDevice); 195 } 196 197 /** 198 * During a call, set active device to "this device". 199 * HeadsetProfile should set to null. 200 * HearingAidProfile should set to null. 201 */ 202 @Test 203 public void setActiveBluetoothDevice_setNull_shouldSetNullToBothProfiles() { 204 mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 205 206 mController.setActiveBluetoothDevice(null); 207 208 verify(mHeadsetProfile).setActiveDevice(null); 209 verify(mHearingAidProfile).setActiveDevice(null); 210 } 211 212 /** 213 * In normal mode 214 * HeadsetProfile should not set active device. 215 */ 216 @Test 217 public void setActiveBluetoothDevice_inNormalMode_shouldNotSetActiveDeviceToHeadsetProfile() { 218 mShadowAudioManager.setMode(AudioManager.MODE_NORMAL); 219 220 mController.setActiveBluetoothDevice(mBluetoothDevice); 221 222 verify(mHeadsetProfile, times(0)).setActiveDevice(any(BluetoothDevice.class)); 223 } 224 225 /** 226 * Default status 227 * Preference should be invisible 228 * Summary should be default summary 229 */ 230 @Test 231 public void updateState_shouldSetSummary() { 232 mController.updateState(mPreference); 233 234 assertThat(mPreference.isVisible()).isFalse(); 235 assertThat(mPreference.getSummary()).isEqualTo( 236 mContext.getText(R.string.media_output_default_summary)); 237 } 238 239 /** 240 * One Hands Free Profile Bluetooth device is available and activated 241 * Preference should be visible 242 * Preference summary should be the activated device name 243 */ 244 @Test 245 public void updateState_oneHeadsetsAvailableAndActivated_shouldSetDeviceName() { 246 mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 247 mShadowAudioManager.setOutputDevice(DEVICE_OUT_BLUETOOTH_SCO); 248 mProfileConnectedDevices.clear(); 249 mProfileConnectedDevices.add(mBluetoothDevice); 250 when(mHeadsetProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 251 when(mHeadsetProfile.getActiveDevice()).thenReturn(mBluetoothDevice); 252 253 mController.updateState(mPreference); 254 255 assertThat(mPreference.isVisible()).isTrue(); 256 assertThat(mPreference.getSummary()).isEqualTo(TEST_DEVICE_NAME_1); 257 } 258 259 /** 260 * More than one Hands Free Profile Bluetooth devices are available, and second 261 * device is active. 262 * Preference should be visible 263 * Preference summary should be the activated device name 264 */ 265 @Test 266 public void updateState_moreThanOneHfpBtDevicesAreAvailable_shouldSetActivatedDeviceName() { 267 mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 268 mShadowAudioManager.setOutputDevice(DEVICE_OUT_BLUETOOTH_SCO); 269 List<BluetoothDevice> connectedDevices = new ArrayList<>(2); 270 connectedDevices.add(mBluetoothDevice); 271 connectedDevices.add(mSecondBluetoothDevice); 272 when(mHeadsetProfile.getConnectedDevices()).thenReturn(connectedDevices); 273 when(mHeadsetProfile.getActiveDevice()).thenReturn(mSecondBluetoothDevice); 274 275 mController.updateState(mPreference); 276 277 assertThat(mPreference.isVisible()).isTrue(); 278 assertThat(mPreference.getSummary()).isEqualTo(TEST_DEVICE_NAME_2); 279 } 280 281 /** 282 * Hands Free Profile Bluetooth device(s) are available, but wired headset is plugged in 283 * and activated. 284 * Preference should be visible 285 * Preference summary should be "This device" 286 */ 287 @Test 288 public void updateState_withAvailableDevicesWiredHeadsetActivated_shouldSetDefaultSummary() { 289 mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 290 mShadowAudioManager.setOutputDevice(DEVICE_OUT_USB_HEADSET); 291 mProfileConnectedDevices.clear(); 292 mProfileConnectedDevices.add(mBluetoothDevice); 293 when(mHeadsetProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 294 when(mHeadsetProfile.getActiveDevice()).thenReturn( 295 mBluetoothDevice); // BT device is still activated in this case 296 297 mController.updateState(mPreference); 298 299 assertThat(mPreference.isVisible()).isTrue(); 300 assertThat(mPreference.getSummary()).isEqualTo( 301 mContext.getText(R.string.media_output_default_summary)); 302 } 303 304 /** 305 * No available Headset BT devices 306 * Preference should be invisible 307 * Preference summary should be "This device" 308 */ 309 @Test 310 public void updateState_noAvailableHeadsetBtDevices_shouldSetDefaultSummary() { 311 mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 312 List<BluetoothDevice> emptyDeviceList = new ArrayList<>(); 313 when(mHeadsetProfile.getConnectedDevices()).thenReturn(emptyDeviceList); 314 315 mController.updateState(mPreference); 316 317 assertThat(mPreference.isVisible()).isFalse(); 318 assertThat(mPreference.getSummary()).isEqualTo( 319 mContext.getText(R.string.media_output_default_summary)); 320 } 321 322 /** 323 * One hearing aid profile Bluetooth device is available and active. 324 * Preference should be visible 325 * Preference summary should be the activated device name 326 */ 327 @Test 328 public void updateState_oneHapBtDeviceAreAvailable_shouldSetActivatedDeviceName() { 329 mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 330 mShadowAudioManager.setOutputDevice(DEVICE_OUT_HEARING_AID); 331 mProfileConnectedDevices.clear(); 332 mProfileConnectedDevices.add(mLeftBluetoothHapDevice); 333 mHearingAidActiveDevices.clear(); 334 mHearingAidActiveDevices.add(mLeftBluetoothHapDevice); 335 when(mHearingAidProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 336 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 337 when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1); 338 339 mController.updateState(mPreference); 340 341 assertThat(mPreference.isVisible()).isTrue(); 342 assertThat(mPreference.getSummary()).isEqualTo(mLeftBluetoothHapDevice.getName()); 343 } 344 345 /** 346 * More than one hearing aid profile Bluetooth devices are available, and second 347 * device is active. 348 * Preference should be visible 349 * Preference summary should be the activated device name 350 */ 351 @Test 352 public void updateState_moreThanOneHapBtDevicesAreAvailable_shouldSetActivatedDeviceName() { 353 mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 354 mShadowAudioManager.setOutputDevice(DEVICE_OUT_HEARING_AID); 355 mProfileConnectedDevices.clear(); 356 mProfileConnectedDevices.add(mLeftBluetoothHapDevice); 357 mProfileConnectedDevices.add(mRightBluetoothHapDevice); 358 mHearingAidActiveDevices.clear(); 359 mHearingAidActiveDevices.add(mRightBluetoothHapDevice); 360 when(mHearingAidProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 361 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 362 when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1); 363 when(mHearingAidProfile.getHiSyncId(mRightBluetoothHapDevice)).thenReturn(HISYNCID2); 364 365 mController.updateState(mPreference); 366 367 assertThat(mPreference.isVisible()).isTrue(); 368 assertThat(mPreference.getSummary()).isEqualTo(mRightBluetoothHapDevice.getName()); 369 } 370 371 /** 372 * Both hearing aid profile and hands free profile Bluetooth devices are available, and 373 * two hearing aid profile devices with same HisyncId. Both of HAP device are active, 374 * "left" side HAP device is added first. 375 * Preference should be visible 376 * Preference summary should be the activated device name 377 * ConnectedDevice should not contain second HAP device with same HisyncId 378 */ 379 @Test 380 public void updateState_hapBtDeviceWithSameId_shouldSetActivatedDeviceName() { 381 mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 382 mShadowAudioManager.setOutputDevice(DEVICE_OUT_HEARING_AID); 383 mProfileConnectedDevices.clear(); 384 mProfileConnectedDevices.add(mBluetoothDevice); 385 //with same HisyncId, only the first one will remain in UI. 386 mProfileConnectedDevices.add(mLeftBluetoothHapDevice); 387 mProfileConnectedDevices.add(mRightBluetoothHapDevice); 388 mHearingAidActiveDevices.clear(); 389 mHearingAidActiveDevices.add(mLeftBluetoothHapDevice); 390 mHearingAidActiveDevices.add(mRightBluetoothHapDevice); 391 when(mHearingAidProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 392 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 393 when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1); 394 when(mHearingAidProfile.getHiSyncId(mRightBluetoothHapDevice)).thenReturn(HISYNCID1); 395 396 mController.updateState(mPreference); 397 398 assertThat(mPreference.isVisible()).isTrue(); 399 assertThat(mPreference.getSummary()).isEqualTo(mLeftBluetoothHapDevice.getName()); 400 assertThat(mController.mConnectedDevices.contains(mLeftBluetoothHapDevice)).isTrue(); 401 assertThat(mController.mConnectedDevices.contains(mRightBluetoothHapDevice)).isFalse(); 402 } 403 404 /** 405 * Both hearing aid profile and hands free profile Bluetooth devices are available, and 406 * two hearing aid profile devices with same HisyncId. Both of HAP device are active, 407 * "right" side HAP device is added first. 408 * Preference should be visible 409 * Preference summary should be the activated device name 410 * ConnectedDevice should not contain second HAP device with same HisyncId 411 */ 412 @Test 413 public void updateState_hapBtDeviceWithSameIdButDifferentOrder_shouldSetActivatedDeviceName() { 414 mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 415 mShadowAudioManager.setOutputDevice(DEVICE_OUT_HEARING_AID); 416 mProfileConnectedDevices.clear(); 417 mProfileConnectedDevices.add(mBluetoothDevice); 418 //with same HisyncId, only the first one will remain in UI. 419 mProfileConnectedDevices.add(mRightBluetoothHapDevice); 420 mProfileConnectedDevices.add(mLeftBluetoothHapDevice); 421 mHearingAidActiveDevices.clear(); 422 mHearingAidActiveDevices.add(mLeftBluetoothHapDevice); 423 mHearingAidActiveDevices.add(mRightBluetoothHapDevice); 424 when(mHearingAidProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 425 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 426 when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1); 427 when(mHearingAidProfile.getHiSyncId(mRightBluetoothHapDevice)).thenReturn(HISYNCID1); 428 429 mController.updateState(mPreference); 430 431 assertThat(mPreference.isVisible()).isTrue(); 432 assertThat(mController.mConnectedDevices.contains(mRightBluetoothHapDevice)).isTrue(); 433 assertThat(mController.mConnectedDevices.contains(mLeftBluetoothHapDevice)).isFalse(); 434 assertThat(mPreference.getSummary()).isEqualTo(mRightBluetoothHapDevice.getName()); 435 } 436 437 /** 438 * Both hearing aid profile and hands free profile Bluetooth devices are available, and 439 * two hearing aid profile devices with different HisyncId. One of HAP device is active. 440 * Preference should be visible 441 * Preference summary should be the activated device name 442 * ConnectedDevice should contain both HAP device with different HisyncId 443 */ 444 @Test 445 public void updateState_hapBtDeviceWithDifferentId_shouldSetActivatedDeviceName() { 446 mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 447 mShadowAudioManager.setOutputDevice(DEVICE_OUT_HEARING_AID); 448 mProfileConnectedDevices.clear(); 449 mProfileConnectedDevices.add(mBluetoothDevice); 450 mProfileConnectedDevices.add(mLeftBluetoothHapDevice); 451 mProfileConnectedDevices.add(mRightBluetoothHapDevice); 452 mHearingAidActiveDevices.clear(); 453 mHearingAidActiveDevices.add(null); 454 mHearingAidActiveDevices.add(mRightBluetoothHapDevice); 455 when(mHearingAidProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 456 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 457 when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1); 458 when(mHearingAidProfile.getHiSyncId(mRightBluetoothHapDevice)).thenReturn(HISYNCID2); 459 460 mController.updateState(mPreference); 461 462 assertThat(mPreference.isVisible()).isTrue(); 463 assertThat(mPreference.getSummary()).isEqualTo(mRightBluetoothHapDevice.getName()); 464 assertThat(mController.mConnectedDevices).containsExactly(mBluetoothDevice, 465 mLeftBluetoothHapDevice, mRightBluetoothHapDevice); 466 } 467} 468