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 17 package com.android.settings.sound; 18 19 import static android.media.AudioSystem.DEVICE_OUT_ALL_SCO; 20 import static android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET; 21 import static android.media.AudioSystem.STREAM_MUSIC; 22 23 import static com.android.settings.core.BasePreferenceController.AVAILABLE; 24 import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; 25 26 import static com.google.common.truth.Truth.assertThat; 27 28 import static org.mockito.ArgumentMatchers.any; 29 import static org.mockito.Mockito.mock; 30 import static org.mockito.Mockito.spy; 31 import static org.mockito.Mockito.verify; 32 import static org.mockito.Mockito.when; 33 34 import android.bluetooth.BluetoothAdapter; 35 import android.bluetooth.BluetoothDevice; 36 import android.bluetooth.BluetoothManager; 37 import android.content.BroadcastReceiver; 38 import android.content.Context; 39 import android.content.IntentFilter; 40 import android.content.pm.PackageManager; 41 import android.media.AudioManager; 42 import android.util.FeatureFlagUtils; 43 44 import androidx.preference.ListPreference; 45 import androidx.preference.PreferenceManager; 46 import androidx.preference.PreferenceScreen; 47 48 import com.android.settings.R; 49 import com.android.settings.bluetooth.Utils; 50 import com.android.settings.core.FeatureFlags; 51 import com.android.settings.testutils.shadow.ShadowAudioManager; 52 import com.android.settings.testutils.shadow.ShadowBluetoothUtils; 53 import com.android.settingslib.bluetooth.A2dpProfile; 54 import com.android.settingslib.bluetooth.BluetoothCallback; 55 import com.android.settingslib.bluetooth.BluetoothEventManager; 56 import com.android.settingslib.bluetooth.HeadsetProfile; 57 import com.android.settingslib.bluetooth.HearingAidProfile; 58 import com.android.settingslib.bluetooth.LocalBluetoothManager; 59 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 60 61 import org.junit.After; 62 import org.junit.Before; 63 import org.junit.Test; 64 import org.junit.runner.RunWith; 65 import org.mockito.Mock; 66 import org.mockito.MockitoAnnotations; 67 import org.robolectric.RobolectricTestRunner; 68 import org.robolectric.RuntimeEnvironment; 69 import org.robolectric.Shadows; 70 import org.robolectric.annotation.Config; 71 import org.robolectric.shadow.api.Shadow; 72 import org.robolectric.shadows.ShadowBluetoothDevice; 73 import org.robolectric.shadows.ShadowPackageManager; 74 75 import java.util.ArrayList; 76 import java.util.List; 77 78 @RunWith(RobolectricTestRunner.class) 79 @Config(shadows = { 80 ShadowAudioManager.class, 81 ShadowBluetoothUtils.class, 82 ShadowBluetoothDevice.class} 83 ) 84 public class AudioOutputSwitchPreferenceControllerTest { 85 private static final String TEST_KEY = "Test_Key"; 86 private static final String TEST_DEVICE_NAME_1 = "Test_A2DP_BT_Device_NAME_1"; 87 private static final String TEST_DEVICE_NAME_2 = "Test_A2DP_BT_Device_NAME_2"; 88 private static final String TEST_DEVICE_ADDRESS_1 = "00:A1:A1:A1:A1:A1"; 89 private static final String TEST_DEVICE_ADDRESS_2 = "00:B2:B2:B2:B2:B2"; 90 private static final String TEST_DEVICE_ADDRESS_3 = "00:C3:C3:C3:C3:C3"; 91 private final static long HISYNCID1 = 10; 92 private final static long HISYNCID2 = 11; 93 94 @Mock 95 private LocalBluetoothManager mLocalManager; 96 @Mock 97 private BluetoothEventManager mBluetoothEventManager; 98 @Mock 99 private LocalBluetoothProfileManager mLocalBluetoothProfileManager; 100 @Mock 101 private A2dpProfile mA2dpProfile; 102 @Mock 103 private HeadsetProfile mHeadsetProfile; 104 @Mock 105 private HearingAidProfile mHearingAidProfile; 106 107 private Context mContext; 108 private PreferenceScreen mScreen; 109 private ListPreference mPreference; 110 private AudioManager mAudioManager; 111 private ShadowAudioManager mShadowAudioManager; 112 private BluetoothManager mBluetoothManager; 113 private BluetoothAdapter mBluetoothAdapter; 114 private BluetoothDevice mBluetoothDevice; 115 private BluetoothDevice mLeftBluetoothHapDevice; 116 private BluetoothDevice mRightBluetoothHapDevice; 117 private LocalBluetoothManager mLocalBluetoothManager; 118 private AudioSwitchPreferenceController mController; 119 private List<BluetoothDevice> mProfileConnectedDevices; 120 private List<BluetoothDevice> mHearingAidActiveDevices; 121 private List<BluetoothDevice> mEmptyDevices; 122 private ShadowPackageManager mPackageManager; 123 124 @Before setUp()125 public void setUp() { 126 MockitoAnnotations.initMocks(this); 127 mContext = spy(RuntimeEnvironment.application); 128 129 mAudioManager = mContext.getSystemService(AudioManager.class); 130 mShadowAudioManager = ShadowAudioManager.getShadow(); 131 132 ShadowBluetoothUtils.sLocalBluetoothManager = mLocalManager; 133 mLocalBluetoothManager = Utils.getLocalBtManager(mContext); 134 135 when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); 136 when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); 137 when(mLocalBluetoothProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile); 138 when(mLocalBluetoothProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile); 139 when(mLocalBluetoothProfileManager.getHeadsetProfile()).thenReturn(mHeadsetProfile); 140 mPackageManager = Shadow.extract(mContext.getPackageManager()); 141 mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true); 142 143 mBluetoothManager = new BluetoothManager(mContext); 144 mBluetoothAdapter = mBluetoothManager.getAdapter(); 145 146 mBluetoothDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_1)); 147 when(mBluetoothDevice.getName()).thenReturn(TEST_DEVICE_NAME_1); 148 when(mBluetoothDevice.isConnected()).thenReturn(true); 149 150 mLeftBluetoothHapDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_2)); 151 when(mLeftBluetoothHapDevice.isConnected()).thenReturn(true); 152 mRightBluetoothHapDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_3)); 153 when(mRightBluetoothHapDevice.isConnected()).thenReturn(true); 154 155 mController = new AudioSwitchPreferenceControllerTestable(mContext, TEST_KEY); 156 mScreen = spy(new PreferenceScreen(mContext, null)); 157 mPreference = new ListPreference(mContext); 158 mProfileConnectedDevices = new ArrayList<>(); 159 mHearingAidActiveDevices = new ArrayList<>(2); 160 mEmptyDevices = new ArrayList<>(2); 161 162 when(mScreen.getPreferenceManager()).thenReturn(mock(PreferenceManager.class)); 163 when(mScreen.getContext()).thenReturn(mContext); 164 when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference); 165 mScreen.addPreference(mPreference); 166 mController.displayPreference(mScreen); 167 } 168 169 @After tearDown()170 public void tearDown() { 171 ShadowBluetoothUtils.reset(); 172 } 173 174 @Test constructor_notSupportBluetooth_shouldReturnBeforeUsingLocalBluetoothManager()175 public void constructor_notSupportBluetooth_shouldReturnBeforeUsingLocalBluetoothManager() { 176 ShadowBluetoothUtils.reset(); 177 mLocalBluetoothManager = Utils.getLocalBtManager(mContext); 178 179 AudioSwitchPreferenceController controller = new AudioSwitchPreferenceControllerTestable( 180 mContext, TEST_KEY); 181 controller.onStart(); 182 controller.onStop(); 183 184 assertThat(mLocalBluetoothManager).isNull(); 185 } 186 187 @Test getAvailabilityStatus_disableFlagNoBluetoothFeature_returnUnavailable()188 public void getAvailabilityStatus_disableFlagNoBluetoothFeature_returnUnavailable() { 189 FeatureFlagUtils.setEnabled(mContext, FeatureFlags.AUDIO_SWITCHER_SETTINGS, false); 190 mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); 191 192 assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); 193 } 194 195 @Test getAvailabilityStatus_disableFlagWithBluetoothFeature_returnUnavailable()196 public void getAvailabilityStatus_disableFlagWithBluetoothFeature_returnUnavailable() { 197 FeatureFlagUtils.setEnabled(mContext, FeatureFlags.AUDIO_SWITCHER_SETTINGS, false); 198 mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true); 199 200 201 assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); 202 } 203 204 @Test getAvailabilityStatus_enableFlagWithBluetoothFeature_returnAvailable()205 public void getAvailabilityStatus_enableFlagWithBluetoothFeature_returnAvailable() { 206 FeatureFlagUtils.setEnabled(mContext, FeatureFlags.AUDIO_SWITCHER_SETTINGS, true); 207 mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true); 208 209 assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); 210 } 211 212 @Test getAvailabilityStatus_enableFlagNoBluetoothFeature_returnUnavailable()213 public void getAvailabilityStatus_enableFlagNoBluetoothFeature_returnUnavailable() { 214 FeatureFlagUtils.setEnabled(mContext, FeatureFlags.AUDIO_SWITCHER_SETTINGS, true); 215 mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); 216 217 assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); 218 } 219 220 @Test onStart_shouldRegisterCallbackAndRegisterReceiver()221 public void onStart_shouldRegisterCallbackAndRegisterReceiver() { 222 mController.onStart(); 223 224 verify(mLocalBluetoothManager.getEventManager()).registerCallback( 225 any(BluetoothCallback.class)); 226 verify(mContext).registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class)); 227 verify(mLocalBluetoothManager).setForegroundActivity(mContext); 228 } 229 230 @Test onStop_shouldUnregisterCallbackAndUnregisterReceiver()231 public void onStop_shouldUnregisterCallbackAndUnregisterReceiver() { 232 mController.onStart(); 233 mController.onStop(); 234 235 verify(mLocalBluetoothManager.getEventManager()).unregisterCallback( 236 any(BluetoothCallback.class)); 237 verify(mContext).unregisterReceiver(any(BroadcastReceiver.class)); 238 verify(mLocalBluetoothManager).setForegroundActivity(null); 239 } 240 241 /** 242 * Audio stream output to bluetooth sco headset which is the subset of all sco device. 243 * isStreamFromOutputDevice should return true. 244 */ 245 @Test isStreamFromOutputDevice_outputDeviceIsBtScoHeadset_shouldReturnTrue()246 public void isStreamFromOutputDevice_outputDeviceIsBtScoHeadset_shouldReturnTrue() { 247 mShadowAudioManager.setOutputDevice(DEVICE_OUT_BLUETOOTH_SCO_HEADSET); 248 249 assertThat(mController.isStreamFromOutputDevice(STREAM_MUSIC, DEVICE_OUT_ALL_SCO)).isTrue(); 250 } 251 252 /** 253 * Left side of HAP device is active. 254 * findActiveHearingAidDevice should return hearing aid device active device. 255 */ 256 @Test findActiveHearingAidDevice_leftActiveDevice_returnLeftDeviceAsActiveHapDevice()257 public void findActiveHearingAidDevice_leftActiveDevice_returnLeftDeviceAsActiveHapDevice() { 258 mController.mConnectedDevices.clear(); 259 mController.mConnectedDevices.add(mBluetoothDevice); 260 mController.mConnectedDevices.add(mLeftBluetoothHapDevice); 261 mHearingAidActiveDevices.clear(); 262 mHearingAidActiveDevices.add(mLeftBluetoothHapDevice); 263 mHearingAidActiveDevices.add(null); 264 when(mHeadsetProfile.getActiveDevice()).thenReturn(mBluetoothDevice); 265 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 266 267 assertThat(mController.findActiveHearingAidDevice()).isEqualTo(mLeftBluetoothHapDevice); 268 } 269 270 /** 271 * Right side of HAP device is active. 272 * findActiveHearingAidDevice should return hearing aid device active device. 273 */ 274 @Test findActiveHearingAidDevice_rightActiveDevice_returnRightDeviceAsActiveHapDevice()275 public void findActiveHearingAidDevice_rightActiveDevice_returnRightDeviceAsActiveHapDevice() { 276 mController.mConnectedDevices.clear(); 277 mController.mConnectedDevices.add(mBluetoothDevice); 278 mController.mConnectedDevices.add(mRightBluetoothHapDevice); 279 mHearingAidActiveDevices.clear(); 280 mHearingAidActiveDevices.add(null); 281 mHearingAidActiveDevices.add(mRightBluetoothHapDevice); 282 when(mHeadsetProfile.getActiveDevice()).thenReturn(mBluetoothDevice); 283 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 284 285 assertThat(mController.findActiveHearingAidDevice()).isEqualTo(mRightBluetoothHapDevice); 286 } 287 288 /** 289 * Both are active device. 290 * findActiveHearingAidDevice only return the active device in mConnectedDevices. 291 */ 292 @Test findActiveHearingAidDevice_twoActiveDevice_returnActiveDeviceInConnectedDevices()293 public void findActiveHearingAidDevice_twoActiveDevice_returnActiveDeviceInConnectedDevices() { 294 mController.mConnectedDevices.clear(); 295 mController.mConnectedDevices.add(mBluetoothDevice); 296 mController.mConnectedDevices.add(mRightBluetoothHapDevice); 297 mHearingAidActiveDevices.clear(); 298 mHearingAidActiveDevices.add(mLeftBluetoothHapDevice); 299 mHearingAidActiveDevices.add(mRightBluetoothHapDevice); 300 when(mHeadsetProfile.getActiveDevice()).thenReturn(mBluetoothDevice); 301 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 302 303 assertThat(mController.findActiveHearingAidDevice()).isEqualTo(mRightBluetoothHapDevice); 304 } 305 306 /** 307 * None of them are active. 308 * findActiveHearingAidDevice should return null. 309 */ 310 @Test findActiveHearingAidDevice_noActiveDevice_returnNull()311 public void findActiveHearingAidDevice_noActiveDevice_returnNull() { 312 mController.mConnectedDevices.clear(); 313 mController.mConnectedDevices.add(mBluetoothDevice); 314 mController.mConnectedDevices.add(mLeftBluetoothHapDevice); 315 mHearingAidActiveDevices.clear(); 316 when(mHeadsetProfile.getActiveDevice()).thenReturn(mBluetoothDevice); 317 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 318 319 assertThat(mController.findActiveHearingAidDevice()).isNull(); 320 } 321 322 /** 323 * Two hearing aid devices with different HisyncId 324 * getConnectedHearingAidDevices should add both device to list. 325 */ 326 @Test getConnectedHearingAidDevices_deviceHisyncIdIsDifferent_shouldAddBothToList()327 public void getConnectedHearingAidDevices_deviceHisyncIdIsDifferent_shouldAddBothToList() { 328 mEmptyDevices.clear(); 329 mProfileConnectedDevices.clear(); 330 mProfileConnectedDevices.add(mLeftBluetoothHapDevice); 331 mProfileConnectedDevices.add(mRightBluetoothHapDevice); 332 when(mHearingAidProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 333 when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1); 334 when(mHearingAidProfile.getHiSyncId(mRightBluetoothHapDevice)).thenReturn(HISYNCID2); 335 336 mEmptyDevices.addAll(mController.getConnectedHearingAidDevices()); 337 338 assertThat(mEmptyDevices).containsExactly(mLeftBluetoothHapDevice, 339 mRightBluetoothHapDevice); 340 } 341 342 /** 343 * Two hearing aid devices with same HisyncId 344 * getConnectedHearingAidDevices should only add first device to list. 345 */ 346 @Test getConnectedHearingAidDevices_deviceHisyncIdIsSame_shouldAddOneToList()347 public void getConnectedHearingAidDevices_deviceHisyncIdIsSame_shouldAddOneToList() { 348 mEmptyDevices.clear(); 349 mProfileConnectedDevices.clear(); 350 mProfileConnectedDevices.add(mLeftBluetoothHapDevice); 351 mProfileConnectedDevices.add(mRightBluetoothHapDevice); 352 when(mHearingAidProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 353 when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1); 354 when(mHearingAidProfile.getHiSyncId(mRightBluetoothHapDevice)).thenReturn(HISYNCID1); 355 356 mEmptyDevices.addAll(mController.getConnectedHearingAidDevices()); 357 358 assertThat(mEmptyDevices).containsExactly(mLeftBluetoothHapDevice); 359 } 360 361 /** 362 * One hands free profile device is connected. 363 * getConnectedA2dpDevices should add this device to list. 364 */ 365 @Test getConnectedHfpDevices_oneConnectedHfpDevice_shouldAddDeviceToList()366 public void getConnectedHfpDevices_oneConnectedHfpDevice_shouldAddDeviceToList() { 367 mEmptyDevices.clear(); 368 mProfileConnectedDevices.clear(); 369 mProfileConnectedDevices.add(mBluetoothDevice); 370 when(mHeadsetProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 371 372 mEmptyDevices.addAll(mController.getConnectedHfpDevices()); 373 374 assertThat(mEmptyDevices).containsExactly(mBluetoothDevice); 375 } 376 377 /** 378 * More than one hands free profile devices are connected. 379 * getConnectedA2dpDevices should add all devices to list. 380 */ 381 @Test getConnectedHfpDevices_moreThanOneConnectedHfpDevice_shouldAddDeviceToList()382 public void getConnectedHfpDevices_moreThanOneConnectedHfpDevice_shouldAddDeviceToList() { 383 mEmptyDevices.clear(); 384 mProfileConnectedDevices.clear(); 385 mProfileConnectedDevices.add(mBluetoothDevice); 386 mProfileConnectedDevices.add(mLeftBluetoothHapDevice); 387 when(mHeadsetProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 388 389 mEmptyDevices.addAll(mController.getConnectedHfpDevices()); 390 391 assertThat(mEmptyDevices).containsExactly(mBluetoothDevice, mLeftBluetoothHapDevice); 392 } 393 394 private class AudioSwitchPreferenceControllerTestable extends 395 AudioSwitchPreferenceController { AudioSwitchPreferenceControllerTestable(Context context, String key)396 AudioSwitchPreferenceControllerTestable(Context context, String key) { 397 super(context, key); 398 } 399 400 @Override findActiveDevice()401 public BluetoothDevice findActiveDevice() { 402 return null; 403 } 404 405 @Override getPreferenceKey()406 public String getPreferenceKey() { 407 return TEST_KEY; 408 } 409 } 410 }