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