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_BLE_HEADSET; 20 import static android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP; 21 import static android.media.AudioSystem.DEVICE_OUT_EARPIECE; 22 import static android.media.AudioSystem.DEVICE_OUT_HEARING_AID; 23 24 import static com.google.common.truth.Truth.assertThat; 25 26 import static org.mockito.ArgumentMatchers.any; 27 import static org.mockito.Mockito.doReturn; 28 import static org.mockito.Mockito.mock; 29 import static org.mockito.Mockito.never; 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.Context; 38 import android.content.Intent; 39 import android.content.pm.ApplicationInfo; 40 import android.content.pm.PackageInfo; 41 import android.content.pm.PackageStats; 42 import android.media.AudioAttributes; 43 import android.media.AudioManager; 44 import android.media.VolumeProvider; 45 import android.media.session.MediaController; 46 import android.media.session.MediaSessionManager; 47 import android.media.session.PlaybackState; 48 49 import androidx.preference.Preference; 50 import androidx.preference.PreferenceManager; 51 import androidx.preference.PreferenceScreen; 52 53 import com.android.settings.R; 54 import com.android.settings.bluetooth.Utils; 55 import com.android.settings.testutils.shadow.ShadowAudioManager; 56 import com.android.settings.testutils.shadow.ShadowBluetoothUtils; 57 import com.android.settingslib.bluetooth.A2dpProfile; 58 import com.android.settingslib.bluetooth.BluetoothEventManager; 59 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 60 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 61 import com.android.settingslib.bluetooth.HearingAidProfile; 62 import com.android.settingslib.bluetooth.LeAudioProfile; 63 import com.android.settingslib.bluetooth.LocalBluetoothManager; 64 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 65 import com.android.settingslib.media.MediaOutputConstants; 66 67 import org.junit.After; 68 import org.junit.Before; 69 import org.junit.Test; 70 import org.junit.runner.RunWith; 71 import org.mockito.ArgumentCaptor; 72 import org.mockito.Mock; 73 import org.mockito.MockitoAnnotations; 74 import org.robolectric.RobolectricTestRunner; 75 import org.robolectric.RuntimeEnvironment; 76 import org.robolectric.Shadows; 77 import org.robolectric.annotation.Config; 78 import org.robolectric.shadows.ShadowBluetoothDevice; 79 import org.robolectric.shadows.ShadowPackageManager; 80 81 import java.util.ArrayList; 82 import java.util.Collection; 83 import java.util.List; 84 85 @RunWith(RobolectricTestRunner.class) 86 @Config(shadows = { 87 ShadowAudioManager.class, 88 ShadowBluetoothUtils.class, 89 ShadowBluetoothDevice.class} 90 ) 91 public class MediaOutputPreferenceControllerTest { 92 private static final String TEST_KEY = "Test_Key"; 93 private static final String TEST_DEVICE_NAME_1 = "Test_A2DP_BT_Device_NAME_1"; 94 private static final String TEST_DEVICE_NAME_2 = "Test_A2DP_BT_Device_NAME_2"; 95 private static final String TEST_HAP_DEVICE_NAME_1 = "Test_HAP_BT_Device_NAME_1"; 96 private static final String TEST_HAP_DEVICE_NAME_2 = "Test_HAP_BT_Device_NAME_2"; 97 private static final String TEST_LE_AUDIO_DEVICE_NAME_1 = "Test_LE_AUDIO_Device_NAME_1"; 98 private static final String TEST_DEVICE_ADDRESS_1 = "00:A1:A1:A1:A1:A1"; 99 private static final String TEST_DEVICE_ADDRESS_2 = "00:B2:B2:B2:B2:B2"; 100 private static final String TEST_DEVICE_ADDRESS_3 = "00:C3:C3:C3:C3:C3"; 101 private static final String TEST_DEVICE_ADDRESS_4 = "00:D4:D4:D4:D4:D4"; 102 private static final String TEST_DEVICE_ADDRESS_5 = "00:E5:E5:E5:E5:E5"; 103 private static final String TEST_PACKAGE_NAME = "com.test.packagename"; 104 private static final String TEST_APPLICATION_LABEL = "APP Test Label"; 105 106 @Mock 107 private LocalBluetoothManager mLocalManager; 108 @Mock 109 private BluetoothEventManager mBluetoothEventManager; 110 @Mock 111 private LocalBluetoothProfileManager mLocalBluetoothProfileManager; 112 @Mock 113 private A2dpProfile mA2dpProfile; 114 @Mock 115 private HearingAidProfile mHearingAidProfile; 116 @Mock 117 private LeAudioProfile mLeAudioProfile; 118 @Mock 119 private AudioSwitchPreferenceController.AudioSwitchCallback mAudioSwitchPreferenceCallback; 120 @Mock 121 private MediaSessionManager mMediaSessionManager; 122 @Mock 123 private MediaController mMediaController; 124 @Mock 125 private CachedBluetoothDeviceManager mCachedDeviceManager; 126 @Mock 127 private CachedBluetoothDevice mCachedBluetoothDeviceL; 128 @Mock 129 private CachedBluetoothDevice mCachedBluetoothDeviceR; 130 131 private Context mContext; 132 private PreferenceScreen mScreen; 133 private Preference mPreference; 134 private AudioManager mAudioManager; 135 private ShadowAudioManager mShadowAudioManager; 136 private BluetoothManager mBluetoothManager; 137 private BluetoothAdapter mBluetoothAdapter; 138 private BluetoothDevice mBluetoothDevice; 139 private BluetoothDevice mSecondBluetoothDevice; 140 private BluetoothDevice mLeftBluetoothHapDevice; 141 private BluetoothDevice mRightBluetoothHapDevice; 142 private LocalBluetoothManager mLocalBluetoothManager; 143 private MediaOutputPreferenceController mController; 144 private List<BluetoothDevice> mProfileConnectedDevices; 145 private List<BluetoothDevice> mHearingAidActiveDevices; 146 private List<BluetoothDevice> mLeAudioActiveDevices; 147 private List<MediaController> mMediaControllers = new ArrayList<>(); 148 private MediaController.PlaybackInfo mPlaybackInfo; 149 private PlaybackState mPlaybackState; 150 private ShadowPackageManager mShadowPackageManager; 151 private ApplicationInfo mAppInfo; 152 private PackageInfo mPackageInfo; 153 private PackageStats mPackageStats; 154 private Collection<CachedBluetoothDevice> mCachedDevices; 155 156 @Before setUp()157 public void setUp() { 158 MockitoAnnotations.initMocks(this); 159 mContext = spy(RuntimeEnvironment.application); 160 161 mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 162 mShadowAudioManager = ShadowAudioManager.getShadow(); 163 164 ShadowBluetoothUtils.sLocalBluetoothManager = mLocalManager; 165 mLocalBluetoothManager = Utils.getLocalBtManager(mContext); 166 167 doReturn(mMediaSessionManager).when(mContext).getSystemService(MediaSessionManager.class); 168 when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers); 169 when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGE_NAME); 170 mPlaybackInfo = new MediaController.PlaybackInfo( 171 MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL, 172 VolumeProvider.VOLUME_CONTROL_ABSOLUTE, 173 100, 174 10, 175 new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(), 176 null); 177 mPlaybackState = new PlaybackState.Builder() 178 .setState(PlaybackState.STATE_PLAYING, 0, 1) 179 .build(); 180 when(mMediaController.getPlaybackInfo()).thenReturn(mPlaybackInfo); 181 when(mMediaController.getPlaybackState()).thenReturn(mPlaybackState); 182 mMediaControllers.add(mMediaController); 183 184 when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); 185 when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); 186 when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager); 187 when(mLocalBluetoothProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile); 188 when(mLocalBluetoothProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile); 189 when(mLocalBluetoothProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile); 190 191 mBluetoothManager = mContext.getSystemService(BluetoothManager.class); 192 mBluetoothAdapter = mBluetoothManager.getAdapter(); 193 194 mCachedDevices = new ArrayList<>(); 195 mCachedDevices.add(mCachedBluetoothDeviceL); 196 mCachedDevices.add(mCachedBluetoothDeviceR); 197 when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices); 198 199 mBluetoothDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_1)); 200 when(mBluetoothDevice.getName()).thenReturn(TEST_DEVICE_NAME_1); 201 when(mBluetoothDevice.getAlias()).thenReturn(TEST_DEVICE_NAME_1); 202 when(mBluetoothDevice.isConnected()).thenReturn(true); 203 204 mSecondBluetoothDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_2)); 205 when(mSecondBluetoothDevice.getName()).thenReturn(TEST_DEVICE_NAME_2); 206 when(mSecondBluetoothDevice.isConnected()).thenReturn(true); 207 208 mLeftBluetoothHapDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_3)); 209 when(mLeftBluetoothHapDevice.getName()).thenReturn(TEST_HAP_DEVICE_NAME_1); 210 when(mLeftBluetoothHapDevice.getAlias()).thenReturn(TEST_HAP_DEVICE_NAME_1); 211 when(mLeftBluetoothHapDevice.isConnected()).thenReturn(true); 212 213 mRightBluetoothHapDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_4)); 214 when(mRightBluetoothHapDevice.getName()).thenReturn(TEST_HAP_DEVICE_NAME_2); 215 when(mRightBluetoothHapDevice.isConnected()).thenReturn(true); 216 217 mController = new MediaOutputPreferenceController(mContext, TEST_KEY); 218 mScreen = spy(new PreferenceScreen(mContext, null)); 219 mPreference = new Preference(mContext); 220 mProfileConnectedDevices = new ArrayList<>(); 221 mHearingAidActiveDevices = new ArrayList<>(2); 222 mLeAudioActiveDevices = new ArrayList<>(); 223 224 when(mScreen.getPreferenceManager()).thenReturn(mock(PreferenceManager.class)); 225 when(mScreen.getContext()).thenReturn(mContext); 226 when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference); 227 mScreen.addPreference(mPreference); 228 mController.displayPreference(mScreen); 229 mController.setCallback(mAudioSwitchPreferenceCallback); 230 } 231 232 @After tearDown()233 public void tearDown() { 234 ShadowBluetoothUtils.reset(); 235 } 236 237 /** 238 * A2DP Bluetooth device(s) are connected, but no device is set as activated 239 * Preference summary should be "This device" 240 */ 241 @Test updateState_withConnectedBtDevice_withoutActiveBtDevice_setDefaultSummary()242 public void updateState_withConnectedBtDevice_withoutActiveBtDevice_setDefaultSummary() { 243 mShadowAudioManager.setOutputDevice(DEVICE_OUT_EARPIECE); 244 mAudioManager.setMode(AudioManager.MODE_NORMAL); 245 mProfileConnectedDevices.clear(); 246 mProfileConnectedDevices.add(mBluetoothDevice); 247 mProfileConnectedDevices.add(mSecondBluetoothDevice); 248 when(mA2dpProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 249 when(mA2dpProfile.getActiveDevice()).thenReturn(null); 250 251 assertThat(mPreference.getSummary()).isNull(); 252 mController.updateState(mPreference); 253 assertThat(mPreference.getSummary()).isEqualTo( 254 mContext.getText(R.string.media_output_default_summary)); 255 } 256 257 /** 258 * A2DP Bluetooth device(s) are connected and active 259 * Preference summary should be device's name 260 */ 261 @Test updateState_withActiveBtDevice_setActivatedDeviceName()262 public void updateState_withActiveBtDevice_setActivatedDeviceName() { 263 mShadowAudioManager.setOutputDevice(DEVICE_OUT_BLUETOOTH_A2DP); 264 mAudioManager.setMode(AudioManager.MODE_NORMAL); 265 mProfileConnectedDevices.clear(); 266 mProfileConnectedDevices.add(mBluetoothDevice); 267 mProfileConnectedDevices.add(mSecondBluetoothDevice); 268 when(mA2dpProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 269 when(mA2dpProfile.getActiveDevice()).thenReturn(mBluetoothDevice); 270 271 assertThat(mPreference.getSummary()).isNull(); 272 mController.updateState(mPreference); 273 assertThat(mPreference.getSummary()).isEqualTo(TEST_DEVICE_NAME_1); 274 } 275 276 /** 277 * Hearing Aid device(s) are connected and active 278 * Preference summary should be device's name 279 */ 280 @Test updateState_withActiveHADevice_setActivatedDeviceName()281 public void updateState_withActiveHADevice_setActivatedDeviceName() { 282 mShadowAudioManager.setOutputDevice(DEVICE_OUT_HEARING_AID); 283 mAudioManager.setMode(AudioManager.MODE_NORMAL); 284 mHearingAidActiveDevices.clear(); 285 mHearingAidActiveDevices.add(mLeftBluetoothHapDevice); 286 when(mHearingAidProfile.getConnectedDevices()).thenReturn(mHearingAidActiveDevices); 287 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 288 289 assertThat(mPreference.getSummary()).isNull(); 290 mController.updateState(mPreference); 291 assertThat(mPreference.getSummary()).isEqualTo(TEST_HAP_DEVICE_NAME_1); 292 293 } 294 295 @Test updateState_withActiveLeAudioDevice_setActivatedDeviceName()296 public void updateState_withActiveLeAudioDevice_setActivatedDeviceName() { 297 mShadowAudioManager.setOutputDevice(DEVICE_OUT_BLE_HEADSET); 298 mAudioManager.setMode(AudioManager.MODE_NORMAL); 299 when(mCachedBluetoothDeviceL.getDevice()).thenReturn(mBluetoothDevice); 300 when(mCachedBluetoothDeviceR.getDevice()).thenReturn(mSecondBluetoothDevice); 301 when(mBluetoothDevice.getAlias()).thenReturn(TEST_LE_AUDIO_DEVICE_NAME_1); 302 mProfileConnectedDevices.clear(); 303 mProfileConnectedDevices.add(mBluetoothDevice); 304 mProfileConnectedDevices.add(mSecondBluetoothDevice); 305 mLeAudioActiveDevices.clear(); 306 mLeAudioActiveDevices.add(mBluetoothDevice); 307 when(mLeAudioProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices); 308 when(mLeAudioProfile.getActiveDevices()).thenReturn(mLeAudioActiveDevices); 309 310 assertThat(mPreference.getSummary()).isNull(); 311 mController.updateState(mPreference); 312 assertThat(mPreference.getSummary()).isEqualTo(TEST_LE_AUDIO_DEVICE_NAME_1); 313 } 314 315 @Test updateState_noActiveLocalPlayback_noTitle()316 public void updateState_noActiveLocalPlayback_noTitle() { 317 mPlaybackState = new PlaybackState.Builder() 318 .setState(PlaybackState.STATE_NONE, 0, 1) 319 .build(); 320 when(mMediaController.getPlaybackState()).thenReturn(mPlaybackState); 321 mController = new MediaOutputPreferenceController(mContext, TEST_KEY); 322 323 mController.updateState(mPreference); 324 325 assertThat(mPreference.getTitle()).isNull(); 326 } 327 328 @Test updateState_withActiveLocalPlayback_checkTitle()329 public void updateState_withActiveLocalPlayback_checkTitle() { 330 initPackage(); 331 mShadowPackageManager.addPackage(mPackageInfo, mPackageStats); 332 333 mController.updateState(mPreference); 334 335 assertThat(mPreference.getTitle()).isEqualTo( 336 mContext.getString(R.string.media_output_label_title, TEST_APPLICATION_LABEL)); 337 } 338 339 @Test click_launch_outputSwitcherSlice()340 public void click_launch_outputSwitcherSlice() { 341 final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); 342 mController.handlePreferenceTreeClick(mPreference); 343 verify(mContext, never()).startActivity(intentCaptor.capture()); 344 345 mPreference.setKey(TEST_KEY); 346 mController.handlePreferenceTreeClick(mPreference); 347 verify(mContext).sendBroadcast(intentCaptor.capture()); 348 assertThat(intentCaptor.getValue().getAction()) 349 .isEqualTo(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG); 350 } 351 352 /** 353 * Default status 354 * Preference should be invisible 355 * Summary should be default summary 356 */ 357 @Test updateState_notInCall_preferenceVisible()358 public void updateState_notInCall_preferenceVisible() { 359 mAudioManager.setMode(AudioManager.MODE_NORMAL); 360 361 mController.updateState(mPreference); 362 363 assertThat(mPreference.isVisible()).isTrue(); 364 } 365 366 /** 367 * During a call 368 * Preference should be invisible 369 * Default string should be "Unavailable during calls" 370 */ 371 @Test updateState_inCall_preferenceInvisible()372 public void updateState_inCall_preferenceInvisible() { 373 mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 374 375 mController.updateState(mPreference); 376 377 assertThat(mPreference.isVisible()).isFalse(); 378 } 379 380 @Test findActiveDevice_onlyA2dpDeviceActive_returnA2dpDevice()381 public void findActiveDevice_onlyA2dpDeviceActive_returnA2dpDevice() { 382 when(mLocalBluetoothProfileManager.getHearingAidProfile()).thenReturn(null); 383 when(mA2dpProfile.getActiveDevice()).thenReturn(mBluetoothDevice); 384 385 assertThat(mController.findActiveDevice()).isEqualTo(mBluetoothDevice); 386 } 387 388 @Test findActiveDevice_allDevicesNotActive_returnNull()389 public void findActiveDevice_allDevicesNotActive_returnNull() { 390 when(mLocalBluetoothProfileManager.getHearingAidProfile()).thenReturn(null); 391 when(mA2dpProfile.getActiveDevice()).thenReturn(null); 392 393 assertThat(mController.findActiveDevice()).isNull(); 394 } 395 396 @Test findActiveDevice_allProfilesWithActiveDevice_returnHADevice()397 public void findActiveDevice_allProfilesWithActiveDevice_returnHADevice() { 398 BluetoothDevice btLeDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_5)); 399 when(btLeDevice.getName()).thenReturn(TEST_LE_AUDIO_DEVICE_NAME_1); 400 mLeAudioActiveDevices.clear(); 401 mLeAudioActiveDevices.add(btLeDevice); 402 mHearingAidActiveDevices.clear(); 403 mHearingAidActiveDevices.add(mLeftBluetoothHapDevice); 404 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 405 when(mA2dpProfile.getActiveDevice()).thenReturn(mBluetoothDevice); 406 when(mLeAudioProfile.getActiveDevices()).thenReturn(mLeAudioActiveDevices); 407 408 assertThat(mController.findActiveDevice()).isEqualTo(mLeftBluetoothHapDevice); 409 } 410 411 @Test findActiveDevice_a2dpDeviceAndLeAudioDeviceActive_returnLeAudioDevice()412 public void findActiveDevice_a2dpDeviceAndLeAudioDeviceActive_returnLeAudioDevice() { 413 BluetoothDevice btLeDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_5)); 414 when(btLeDevice.getName()).thenReturn(TEST_LE_AUDIO_DEVICE_NAME_1); 415 mLeAudioActiveDevices.clear(); 416 mLeAudioActiveDevices.add(btLeDevice); 417 mHearingAidActiveDevices.clear(); 418 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 419 when(mA2dpProfile.getActiveDevice()).thenReturn(mBluetoothDevice); 420 when(mLeAudioProfile.getActiveDevices()).thenReturn(mLeAudioActiveDevices); 421 422 assertThat(mController.findActiveDevice()).isEqualTo(btLeDevice); 423 } 424 425 @Test findActiveDevice_onlyLeAudioDeviceActive_returnLeAudioDevice()426 public void findActiveDevice_onlyLeAudioDeviceActive_returnLeAudioDevice() { 427 BluetoothDevice btLeDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_5)); 428 when(btLeDevice.getName()).thenReturn(TEST_LE_AUDIO_DEVICE_NAME_1); 429 mLeAudioActiveDevices.clear(); 430 mLeAudioActiveDevices.add(btLeDevice); 431 mHearingAidActiveDevices.clear(); 432 when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices); 433 when(mA2dpProfile.getActiveDevice()).thenReturn(null); 434 when(mLeAudioProfile.getActiveDevices()).thenReturn(mLeAudioActiveDevices); 435 436 assertThat(mController.findActiveDevice()).isEqualTo(btLeDevice); 437 } 438 initPackage()439 private void initPackage() { 440 mShadowPackageManager = Shadows.shadowOf(mContext.getPackageManager()); 441 mAppInfo = new ApplicationInfo(); 442 mAppInfo.flags = ApplicationInfo.FLAG_INSTALLED; 443 mAppInfo.packageName = TEST_PACKAGE_NAME; 444 mAppInfo.name = TEST_APPLICATION_LABEL; 445 mPackageInfo = new PackageInfo(); 446 mPackageInfo.packageName = TEST_PACKAGE_NAME; 447 mPackageInfo.applicationInfo = mAppInfo; 448 mPackageStats = new PackageStats(TEST_PACKAGE_NAME); 449 } 450 } 451