1 /* 2 * Copyright (C) 2022 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.bluetooth; 18 19 import static android.media.Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL; 20 import static android.media.Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; 21 22 import static com.google.common.truth.Truth.assertThat; 23 24 import static org.mockito.ArgumentMatchers.any; 25 import static org.mockito.Mockito.spy; 26 import static org.mockito.Mockito.verify; 27 import static org.mockito.Mockito.when; 28 29 import android.app.settings.SettingsEnums; 30 import android.bluetooth.BluetoothDevice; 31 import android.bluetooth.BluetoothProfile; 32 import android.media.AudioDeviceAttributes; 33 import android.media.AudioDeviceInfo; 34 import android.media.AudioManager; 35 import android.media.Spatializer; 36 import android.platform.test.annotations.EnableFlags; 37 import android.platform.test.flag.junit.SetFlagsRule; 38 39 import androidx.preference.PreferenceCategory; 40 import androidx.preference.TwoStatePreference; 41 42 import com.android.settings.testutils.FakeFeatureFactory; 43 import com.android.settingslib.bluetooth.A2dpProfile; 44 import com.android.settingslib.bluetooth.HearingAidProfile; 45 import com.android.settingslib.bluetooth.LeAudioProfile; 46 import com.android.settingslib.core.lifecycle.Lifecycle; 47 import com.android.settingslib.flags.Flags; 48 49 import com.google.common.collect.ImmutableList; 50 51 import org.junit.Before; 52 import org.junit.Rule; 53 import org.junit.Test; 54 import org.junit.runner.RunWith; 55 import org.mockito.Mock; 56 import org.mockito.MockitoAnnotations; 57 import org.robolectric.RobolectricTestRunner; 58 import org.robolectric.RuntimeEnvironment; 59 import org.robolectric.shadows.ShadowLooper; 60 61 import java.util.ArrayList; 62 import java.util.List; 63 64 @RunWith(RobolectricTestRunner.class) 65 public class BluetoothDetailsSpatialAudioControllerTest extends BluetoothDetailsControllerTestBase { 66 @Rule 67 public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); 68 private static final String MAC_ADDRESS = "04:52:C7:0B:D8:3C"; 69 private static final String KEY_SPATIAL_AUDIO = "spatial_audio"; 70 private static final String KEY_HEAD_TRACKING = "head_tracking"; 71 72 @Mock private AudioManager mAudioManager; 73 @Mock private Spatializer mSpatializer; 74 @Mock private Lifecycle mSpatialAudioLifecycle; 75 @Mock private PreferenceCategory mProfilesContainer; 76 @Mock private BluetoothDevice mBluetoothDevice; 77 @Mock private A2dpProfile mA2dpProfile; 78 @Mock private LeAudioProfile mLeAudioProfile; 79 @Mock private HearingAidProfile mHearingAidProfile; 80 81 private AudioDeviceAttributes mAvailableDevice; 82 83 private BluetoothDetailsSpatialAudioController mController; 84 private TwoStatePreference mSpatialAudioPref; 85 private TwoStatePreference mHeadTrackingPref; 86 private FakeFeatureFactory mFeatureFactory; 87 88 @Before setUp()89 public void setUp() { 90 MockitoAnnotations.initMocks(this); 91 92 mContext = spy(RuntimeEnvironment.application); 93 mFeatureFactory = FakeFeatureFactory.setupForTest(); 94 95 when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager); 96 when(mAudioManager.getSpatializer()).thenReturn(mSpatializer); 97 when(mCachedDevice.getAddress()).thenReturn(MAC_ADDRESS); 98 when(mCachedDevice.getDevice()).thenReturn(mBluetoothDevice); 99 when(mCachedDevice.getProfiles()) 100 .thenReturn(List.of(mA2dpProfile, mLeAudioProfile, mHearingAidProfile)); 101 when(mA2dpProfile.isEnabled(mBluetoothDevice)).thenReturn(true); 102 when(mA2dpProfile.getProfileId()).thenReturn(BluetoothProfile.A2DP); 103 when(mLeAudioProfile.getProfileId()).thenReturn(BluetoothProfile.LE_AUDIO); 104 when(mHearingAidProfile.getProfileId()).thenReturn(BluetoothProfile.HEARING_AID); 105 when(mBluetoothDevice.getAnonymizedAddress()).thenReturn(MAC_ADDRESS); 106 when(mFeatureFactory.getBluetoothFeatureProvider().getSpatializer(mContext)) 107 .thenReturn(mSpatializer); 108 109 mController = 110 new BluetoothDetailsSpatialAudioController( 111 mContext, mFragment, mCachedDevice, mSpatialAudioLifecycle); 112 mController.mProfilesContainer = mProfilesContainer; 113 114 mSpatialAudioPref = mController.createSpatialAudioPreference(mContext); 115 mHeadTrackingPref = mController.createHeadTrackingPreference(mContext); 116 117 when(mProfilesContainer.findPreference(KEY_SPATIAL_AUDIO)).thenReturn(mSpatialAudioPref); 118 when(mProfilesContainer.findPreference(KEY_HEAD_TRACKING)).thenReturn(mHeadTrackingPref); 119 120 mAvailableDevice = 121 new AudioDeviceAttributes( 122 AudioDeviceAttributes.ROLE_OUTPUT, 123 AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, 124 MAC_ADDRESS); 125 } 126 127 @Test isAvailable_forSpatializerWithLevelNone_returnsFalse()128 public void isAvailable_forSpatializerWithLevelNone_returnsFalse() { 129 when(mSpatializer.getImmersiveAudioLevel()).thenReturn(SPATIALIZER_IMMERSIVE_LEVEL_NONE); 130 assertThat(mController.isAvailable()).isFalse(); 131 } 132 133 @Test isAvailable_forSpatializerWithLevelNotNone_returnsTrue()134 public void isAvailable_forSpatializerWithLevelNotNone_returnsTrue() { 135 when(mSpatializer.getImmersiveAudioLevel()) 136 .thenReturn(SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL); 137 assertThat(mController.isAvailable()).isTrue(); 138 } 139 140 @Test refresh_spatialAudioIsTurnedOn_checksSpatialAudioPreference()141 public void refresh_spatialAudioIsTurnedOn_checksSpatialAudioPreference() { 142 List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>(); 143 mController.setAvailableDevice(mAvailableDevice); 144 compatibleAudioDevices.add(mController.mAudioDevice); 145 when(mSpatializer.isAvailableForDevice(mController.mAudioDevice)).thenReturn(true); 146 when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices); 147 148 mController.refresh(); 149 ShadowLooper.idleMainLooper(); 150 151 assertThat(mSpatialAudioPref.isChecked()).isTrue(); 152 } 153 154 @Test refresh_spatialAudioIsTurnedOff_unchecksSpatialAudioPreference()155 public void refresh_spatialAudioIsTurnedOff_unchecksSpatialAudioPreference() { 156 List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>(); 157 when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices); 158 159 mController.refresh(); 160 ShadowLooper.idleMainLooper(); 161 162 assertThat(mSpatialAudioPref.isChecked()).isFalse(); 163 } 164 165 @Test refresh_spatialAudioOnAndHeadTrackingIsAvailable_showsHeadTrackingPreference()166 public void refresh_spatialAudioOnAndHeadTrackingIsAvailable_showsHeadTrackingPreference() { 167 List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>(); 168 compatibleAudioDevices.add(mController.mAudioDevice); 169 when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices); 170 when(mSpatializer.hasHeadTracker(mController.mAudioDevice)).thenReturn(true); 171 172 mController.refresh(); 173 ShadowLooper.idleMainLooper(); 174 175 assertThat(mHeadTrackingPref.isVisible()).isTrue(); 176 } 177 178 @Test refresh_spatialAudioOnHeadTrackingOff_recordMetrics()179 public void refresh_spatialAudioOnHeadTrackingOff_recordMetrics() { 180 mController.setAvailableDevice(mAvailableDevice); 181 when(mSpatializer.isAvailableForDevice(mAvailableDevice)).thenReturn(true); 182 when(mSpatializer.getCompatibleAudioDevices()) 183 .thenReturn(ImmutableList.of(mAvailableDevice)); 184 when(mSpatializer.hasHeadTracker(mAvailableDevice)).thenReturn(true); 185 when(mSpatializer.isHeadTrackerEnabled(mController.mAudioDevice)).thenReturn(false); 186 187 mController.refresh(); 188 ShadowLooper.idleMainLooper(); 189 190 verify(mFeatureFactory.metricsFeatureProvider) 191 .action( 192 mContext, 193 SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_SPATIAL_AUDIO_TRIGGERED, 194 true); 195 verify(mFeatureFactory.metricsFeatureProvider) 196 .action( 197 mContext, 198 SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_HEAD_TRACKING_TRIGGERED, 199 false); 200 } 201 202 @Test refresh_spatialAudioOff_recordMetrics()203 public void refresh_spatialAudioOff_recordMetrics() { 204 mController.setAvailableDevice(mAvailableDevice); 205 when(mSpatializer.isAvailableForDevice(mAvailableDevice)).thenReturn(true); 206 when(mSpatializer.getCompatibleAudioDevices()).thenReturn(ImmutableList.of()); 207 when(mSpatializer.hasHeadTracker(mAvailableDevice)).thenReturn(true); 208 when(mSpatializer.isHeadTrackerEnabled(mController.mAudioDevice)).thenReturn(false); 209 210 mController.refresh(); 211 ShadowLooper.idleMainLooper(); 212 213 verify(mFeatureFactory.metricsFeatureProvider) 214 .action( 215 mContext, 216 SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_SPATIAL_AUDIO_TRIGGERED, 217 false); 218 verify(mFeatureFactory.metricsFeatureProvider) 219 .action( 220 mContext, 221 SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_HEAD_TRACKING_TRIGGERED, 222 false); 223 } 224 225 @Test refresh_spatialAudioOnAndHeadTrackingIsNotAvailable_hidesHeadTrackingPreference()226 public void refresh_spatialAudioOnAndHeadTrackingIsNotAvailable_hidesHeadTrackingPreference() { 227 mController.setAvailableDevice(mAvailableDevice); 228 when(mSpatializer.isAvailableForDevice(mAvailableDevice)).thenReturn(true); 229 when(mSpatializer.getCompatibleAudioDevices()) 230 .thenReturn(ImmutableList.of(mAvailableDevice)); 231 when(mSpatializer.hasHeadTracker(mAvailableDevice)).thenReturn(false); 232 233 mController.refresh(); 234 ShadowLooper.idleMainLooper(); 235 236 assertThat(mHeadTrackingPref.isVisible()).isFalse(); 237 } 238 239 @Test refresh_spatialAudioOff_hidesHeadTrackingPreference()240 public void refresh_spatialAudioOff_hidesHeadTrackingPreference() { 241 mController.setAvailableDevice(mAvailableDevice); 242 when(mSpatializer.isAvailableForDevice(mAvailableDevice)).thenReturn(true); 243 when(mSpatializer.getCompatibleAudioDevices()).thenReturn(ImmutableList.of()); 244 when(mSpatializer.hasHeadTracker(mAvailableDevice)).thenReturn(true); 245 246 mController.refresh(); 247 ShadowLooper.idleMainLooper(); 248 249 assertThat(mHeadTrackingPref.isVisible()).isFalse(); 250 } 251 252 @Test refresh_headTrackingIsTurnedOn_checksHeadTrackingPreference()253 public void refresh_headTrackingIsTurnedOn_checksHeadTrackingPreference() { 254 List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>(); 255 mController.setAvailableDevice(mAvailableDevice); 256 compatibleAudioDevices.add(mController.mAudioDevice); 257 when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices); 258 when(mSpatializer.isAvailableForDevice(mController.mAudioDevice)).thenReturn(true); 259 when(mSpatializer.hasHeadTracker(mController.mAudioDevice)).thenReturn(true); 260 when(mSpatializer.isHeadTrackerEnabled(mController.mAudioDevice)).thenReturn(true); 261 262 mController.refresh(); 263 ShadowLooper.idleMainLooper(); 264 265 assertThat(mHeadTrackingPref.isChecked()).isTrue(); 266 verify(mFeatureFactory.metricsFeatureProvider) 267 .action( 268 mContext, 269 SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_HEAD_TRACKING_TRIGGERED, 270 true); 271 } 272 273 @Test refresh_headTrackingIsTurnedOff_unchecksHeadTrackingPreference()274 public void refresh_headTrackingIsTurnedOff_unchecksHeadTrackingPreference() { 275 List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>(); 276 mController.setAvailableDevice(mAvailableDevice); 277 compatibleAudioDevices.add(mController.mAudioDevice); 278 when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices); 279 when(mSpatializer.isAvailableForDevice(mController.mAudioDevice)).thenReturn(true); 280 when(mSpatializer.hasHeadTracker(mController.mAudioDevice)).thenReturn(true); 281 when(mSpatializer.isHeadTrackerEnabled(mController.mAudioDevice)).thenReturn(false); 282 283 mController.refresh(); 284 ShadowLooper.idleMainLooper(); 285 286 assertThat(mHeadTrackingPref.isChecked()).isFalse(); 287 verify(mFeatureFactory.metricsFeatureProvider) 288 .action( 289 mContext, 290 SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_HEAD_TRACKING_TRIGGERED, 291 false); 292 } 293 294 @Test 295 @EnableFlags(Flags.FLAG_ENABLE_DETERMINING_SPATIAL_AUDIO_ATTRIBUTES_BY_PROFILE) refresh_leAudioProfileEnabledForHeadset_useLeAudioHeadsetAttributes()296 public void refresh_leAudioProfileEnabledForHeadset_useLeAudioHeadsetAttributes() { 297 when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(true); 298 when(mA2dpProfile.isEnabled(mBluetoothDevice)).thenReturn(false); 299 when(mHearingAidProfile.isEnabled(mBluetoothDevice)).thenReturn(false); 300 when(mAudioManager.getBluetoothAudioDeviceCategory(MAC_ADDRESS)) 301 .thenReturn(AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES); 302 when(mSpatializer.isAvailableForDevice(any())).thenReturn(true); 303 304 mController.refresh(); 305 ShadowLooper.idleMainLooper(); 306 307 assertThat(mController.mAudioDevice.getType()).isEqualTo(AudioDeviceInfo.TYPE_BLE_HEADSET); 308 } 309 310 @Test 311 @EnableFlags(Flags.FLAG_ENABLE_DETERMINING_SPATIAL_AUDIO_ATTRIBUTES_BY_PROFILE) refresh_leAudioProfileEnabledForSpeaker_useLeAudioSpeakerAttributes()312 public void refresh_leAudioProfileEnabledForSpeaker_useLeAudioSpeakerAttributes() { 313 when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(true); 314 when(mA2dpProfile.isEnabled(mBluetoothDevice)).thenReturn(false); 315 when(mHearingAidProfile.isEnabled(mBluetoothDevice)).thenReturn(false); 316 when(mAudioManager.getBluetoothAudioDeviceCategory(MAC_ADDRESS)) 317 .thenReturn(AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER); 318 when(mSpatializer.isAvailableForDevice(any())).thenReturn(true); 319 320 mController.refresh(); 321 ShadowLooper.idleMainLooper(); 322 323 assertThat(mController.mAudioDevice.getType()).isEqualTo(AudioDeviceInfo.TYPE_BLE_SPEAKER); 324 } 325 326 @Test 327 @EnableFlags(Flags.FLAG_ENABLE_DETERMINING_SPATIAL_AUDIO_ATTRIBUTES_BY_PROFILE) refresh_hearingAidProfileEnabled_useHearingAidAttributes()328 public void refresh_hearingAidProfileEnabled_useHearingAidAttributes() { 329 when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(false); 330 when(mA2dpProfile.isEnabled(mBluetoothDevice)).thenReturn(false); 331 when(mHearingAidProfile.isEnabled(mBluetoothDevice)).thenReturn(true); 332 when(mSpatializer.isAvailableForDevice(any())).thenReturn(true); 333 334 mController.refresh(); 335 ShadowLooper.idleMainLooper(); 336 337 assertThat(mController.mAudioDevice.getType()).isEqualTo(AudioDeviceInfo.TYPE_HEARING_AID); 338 } 339 340 @Test turnedOnSpatialAudio_invokesAddCompatibleAudioDevice()341 public void turnedOnSpatialAudio_invokesAddCompatibleAudioDevice() { 342 mController.setAvailableDevice(mAvailableDevice); 343 mSpatialAudioPref.setChecked(true); 344 mController.onPreferenceClick(mSpatialAudioPref); 345 verify(mSpatializer).addCompatibleAudioDevice(mController.mAudioDevice); 346 verify(mFeatureFactory.metricsFeatureProvider) 347 .action( 348 mContext, 349 SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_SPATIAL_AUDIO_TOGGLE_CLICKED, 350 true); 351 } 352 353 @Test turnedOffSpatialAudio_invokesRemoveCompatibleAudioDevice()354 public void turnedOffSpatialAudio_invokesRemoveCompatibleAudioDevice() { 355 mController.setAvailableDevice(mAvailableDevice); 356 mSpatialAudioPref.setChecked(false); 357 mController.onPreferenceClick(mSpatialAudioPref); 358 verify(mSpatializer).removeCompatibleAudioDevice(mController.mAudioDevice); 359 verify(mFeatureFactory.metricsFeatureProvider) 360 .action( 361 mContext, 362 SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_SPATIAL_AUDIO_TOGGLE_CLICKED, 363 false); 364 } 365 366 @Test turnedOnHeadTracking_invokesSetHeadTrackerEnabled_setsTrue()367 public void turnedOnHeadTracking_invokesSetHeadTrackerEnabled_setsTrue() { 368 mController.setAvailableDevice(mAvailableDevice); 369 mHeadTrackingPref.setChecked(true); 370 mController.onPreferenceClick(mHeadTrackingPref); 371 verify(mSpatializer).setHeadTrackerEnabled(true, mController.mAudioDevice); 372 verify(mFeatureFactory.metricsFeatureProvider) 373 .action( 374 mContext, 375 SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_HEAD_TRACKING_TOGGLE_CLICKED, 376 true); 377 } 378 379 @Test turnedOffHeadTracking_invokesSetHeadTrackerEnabled_setsFalse()380 public void turnedOffHeadTracking_invokesSetHeadTrackerEnabled_setsFalse() { 381 mController.setAvailableDevice(mAvailableDevice); 382 mHeadTrackingPref.setChecked(false); 383 mController.onPreferenceClick(mHeadTrackingPref); 384 verify(mSpatializer).setHeadTrackerEnabled(false, mController.mAudioDevice); 385 verify(mFeatureFactory.metricsFeatureProvider) 386 .action( 387 mContext, 388 SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_HEAD_TRACKING_TOGGLE_CLICKED, 389 false); 390 } 391 } 392