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