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
17package com.android.settings.sound;
18
19import static android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_SCO;
20import static android.media.AudioSystem.DEVICE_OUT_HEARING_AID;
21import static android.media.AudioSystem.DEVICE_OUT_USB_HEADSET;
22
23import static com.google.common.truth.Truth.assertThat;
24
25import static org.mockito.ArgumentMatchers.any;
26import static org.mockito.Mockito.mock;
27import static org.mockito.Mockito.spy;
28import static org.mockito.Mockito.times;
29import static org.mockito.Mockito.verify;
30import static org.mockito.Mockito.when;
31
32import android.bluetooth.BluetoothAdapter;
33import android.bluetooth.BluetoothDevice;
34import android.bluetooth.BluetoothManager;
35import android.content.Context;
36import android.media.AudioManager;
37import android.support.v7.preference.ListPreference;
38import android.support.v7.preference.PreferenceManager;
39import android.support.v7.preference.PreferenceScreen;
40
41import com.android.settings.R;
42import com.android.settings.testutils.SettingsRobolectricTestRunner;
43import com.android.settings.testutils.shadow.ShadowAudioManager;
44import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
45import com.android.settings.testutils.shadow.ShadowMediaRouter;
46import com.android.settingslib.bluetooth.BluetoothEventManager;
47import com.android.settingslib.bluetooth.HeadsetProfile;
48import com.android.settingslib.bluetooth.HearingAidProfile;
49import com.android.settingslib.bluetooth.LocalBluetoothManager;
50import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
51
52import org.junit.After;
53import org.junit.Before;
54import org.junit.Test;
55import org.junit.runner.RunWith;
56import org.mockito.Mock;
57import org.mockito.MockitoAnnotations;
58import org.robolectric.RuntimeEnvironment;
59import org.robolectric.annotation.Config;
60import org.robolectric.shadows.ShadowBluetoothDevice;
61
62import java.util.ArrayList;
63import java.util.List;
64
65@RunWith(SettingsRobolectricTestRunner.class)
66@Config(shadows = {
67        ShadowAudioManager.class,
68        ShadowMediaRouter.class,
69        ShadowBluetoothUtils.class,
70        ShadowBluetoothDevice.class}
71)
72public class HandsFreeProfileOutputPreferenceControllerTest {
73    private static final String TEST_KEY = "Test_Key";
74    private static final String TEST_DEVICE_NAME_1 = "Test_HFP_BT_Device_NAME_1";
75    private static final String TEST_DEVICE_NAME_2 = "Test_HFP_BT_Device_NAME_2";
76    private static final String TEST_HAP_DEVICE_NAME_1 = "Test_HAP_BT_Device_NAME_1";
77    private static final String TEST_HAP_DEVICE_NAME_2 = "Test_HAP_BT_Device_NAME_2";
78    private static final String TEST_DEVICE_ADDRESS_1 = "00:A1:A1:A1:A1:A1";
79    private static final String TEST_DEVICE_ADDRESS_2 = "00:B2:B2:B2:B2:B2";
80    private static final String TEST_DEVICE_ADDRESS_3 = "00:C3:C3:C3:C3:C3";
81    private static final String TEST_DEVICE_ADDRESS_4 = "00:D4:D4:D4:D4:D4";
82    private final static long HISYNCID1 = 10;
83    private final static long HISYNCID2 = 11;
84
85    @Mock
86    private LocalBluetoothManager mLocalManager;
87    @Mock
88    private BluetoothEventManager mBluetoothEventManager;
89    @Mock
90    private LocalBluetoothProfileManager mLocalBluetoothProfileManager;
91    @Mock
92    private HeadsetProfile mHeadsetProfile;
93    @Mock
94    private HearingAidProfile mHearingAidProfile;
95    @Mock
96    private AudioSwitchPreferenceController.AudioSwitchCallback mAudioSwitchPreferenceCallback;
97
98    private Context mContext;
99    private PreferenceScreen mScreen;
100    private ListPreference mPreference;
101    private ShadowAudioManager mShadowAudioManager;
102    private ShadowMediaRouter mShadowMediaRouter;
103    private BluetoothManager mBluetoothManager;
104    private BluetoothAdapter mBluetoothAdapter;
105    private BluetoothDevice mBluetoothDevice;
106    private BluetoothDevice mSecondBluetoothDevice;
107    private BluetoothDevice mLeftBluetoothHapDevice;
108    private BluetoothDevice mRightBluetoothHapDevice;
109    private LocalBluetoothManager mLocalBluetoothManager;
110    private AudioSwitchPreferenceController mController;
111    private List<BluetoothDevice> mProfileConnectedDevices;
112    private List<BluetoothDevice> mHearingAidActiveDevices;
113
114    @Before
115    public void setUp() {
116        MockitoAnnotations.initMocks(this);
117        mContext = spy(RuntimeEnvironment.application);
118
119        mShadowAudioManager = ShadowAudioManager.getShadow();
120        mShadowMediaRouter = ShadowMediaRouter.getShadow();
121
122        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalManager;
123        mLocalBluetoothManager = ShadowBluetoothUtils.getLocalBtManager(mContext);
124
125        when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager);
126        when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager);
127        when(mLocalBluetoothProfileManager.getHeadsetProfile()).thenReturn(mHeadsetProfile);
128        when(mLocalBluetoothProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
129
130        mBluetoothManager = new BluetoothManager(mContext);
131        mBluetoothAdapter = mBluetoothManager.getAdapter();
132
133        mBluetoothDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_1));
134        when(mBluetoothDevice.getName()).thenReturn(TEST_DEVICE_NAME_1);
135        when(mBluetoothDevice.isConnected()).thenReturn(true);
136
137        mSecondBluetoothDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_2));
138        when(mSecondBluetoothDevice.getName()).thenReturn(TEST_DEVICE_NAME_2);
139        when(mSecondBluetoothDevice.isConnected()).thenReturn(true);
140
141        mLeftBluetoothHapDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_3));
142        when(mLeftBluetoothHapDevice.getName()).thenReturn(TEST_HAP_DEVICE_NAME_1);
143        when(mLeftBluetoothHapDevice.isConnected()).thenReturn(true);
144
145        mRightBluetoothHapDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_4));
146        when(mRightBluetoothHapDevice.getName()).thenReturn(TEST_HAP_DEVICE_NAME_2);
147        when(mRightBluetoothHapDevice.isConnected()).thenReturn(true);
148
149        mController = new HandsFreeProfileOutputPreferenceController(mContext, TEST_KEY);
150        mScreen = spy(new PreferenceScreen(mContext, null));
151        mPreference = new ListPreference(mContext);
152        mProfileConnectedDevices = new ArrayList<>();
153        mHearingAidActiveDevices = new ArrayList<>(2);
154
155        when(mScreen.getPreferenceManager()).thenReturn(mock(PreferenceManager.class));
156        when(mScreen.getContext()).thenReturn(mContext);
157        when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
158        mScreen.addPreference(mPreference);
159        mController.displayPreference(mScreen);
160        mController.setCallback(mAudioSwitchPreferenceCallback);
161    }
162
163    @After
164    public void tearDown() {
165        mShadowAudioManager.reset();
166        mShadowMediaRouter.reset();
167        ShadowBluetoothUtils.reset();
168    }
169
170    /**
171     * During a call, bluetooth device with HisyncId.
172     * HearingAidProfile should set active device to this device.
173     */
174    @Test
175    public void setActiveBluetoothDevice_btDeviceWithHisyncId_shouldSetBtDeviceActive() {
176        mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
177        when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1);
178
179        mController.setActiveBluetoothDevice(mLeftBluetoothHapDevice);
180
181        verify(mHearingAidProfile).setActiveDevice(mLeftBluetoothHapDevice);
182    }
183
184    /**
185     * During a call, Bluetooth device without HisyncId.
186     * HeadsetProfile should set active device to this device.
187     */
188    @Test
189    public void setActiveBluetoothDevice_btDeviceWithoutHisyncId_shouldSetBtDeviceActive() {
190        mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
191
192        mController.setActiveBluetoothDevice(mBluetoothDevice);
193
194        verify(mHeadsetProfile).setActiveDevice(mBluetoothDevice);
195    }
196
197    /**
198     * During a call, set active device to "this device".
199     * HeadsetProfile should set to null.
200     * HearingAidProfile should set to null.
201     */
202    @Test
203    public void setActiveBluetoothDevice_setNull_shouldSetNullToBothProfiles() {
204        mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
205
206        mController.setActiveBluetoothDevice(null);
207
208        verify(mHeadsetProfile).setActiveDevice(null);
209        verify(mHearingAidProfile).setActiveDevice(null);
210    }
211
212    /**
213     * In normal mode
214     * HeadsetProfile should not set active device.
215     */
216    @Test
217    public void setActiveBluetoothDevice_inNormalMode_shouldNotSetActiveDeviceToHeadsetProfile() {
218        mShadowAudioManager.setMode(AudioManager.MODE_NORMAL);
219
220        mController.setActiveBluetoothDevice(mBluetoothDevice);
221
222        verify(mHeadsetProfile, times(0)).setActiveDevice(any(BluetoothDevice.class));
223    }
224
225    /**
226     * Default status
227     * Preference should be invisible
228     * Summary should be default summary
229     */
230    @Test
231    public void updateState_shouldSetSummary() {
232        mController.updateState(mPreference);
233
234        assertThat(mPreference.isVisible()).isFalse();
235        assertThat(mPreference.getSummary()).isEqualTo(
236                mContext.getText(R.string.media_output_default_summary));
237    }
238
239    /**
240     * One Hands Free Profile Bluetooth device is available and activated
241     * Preference should be visible
242     * Preference summary should be the activated device name
243     */
244    @Test
245    public void updateState_oneHeadsetsAvailableAndActivated_shouldSetDeviceName() {
246        mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
247        mShadowAudioManager.setOutputDevice(DEVICE_OUT_BLUETOOTH_SCO);
248        mProfileConnectedDevices.clear();
249        mProfileConnectedDevices.add(mBluetoothDevice);
250        when(mHeadsetProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices);
251        when(mHeadsetProfile.getActiveDevice()).thenReturn(mBluetoothDevice);
252
253        mController.updateState(mPreference);
254
255        assertThat(mPreference.isVisible()).isTrue();
256        assertThat(mPreference.getSummary()).isEqualTo(TEST_DEVICE_NAME_1);
257    }
258
259    /**
260     * More than one Hands Free Profile Bluetooth devices are available, and second
261     * device is active.
262     * Preference should be visible
263     * Preference summary should be the activated device name
264     */
265    @Test
266    public void updateState_moreThanOneHfpBtDevicesAreAvailable_shouldSetActivatedDeviceName() {
267        mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
268        mShadowAudioManager.setOutputDevice(DEVICE_OUT_BLUETOOTH_SCO);
269        List<BluetoothDevice> connectedDevices = new ArrayList<>(2);
270        connectedDevices.add(mBluetoothDevice);
271        connectedDevices.add(mSecondBluetoothDevice);
272        when(mHeadsetProfile.getConnectedDevices()).thenReturn(connectedDevices);
273        when(mHeadsetProfile.getActiveDevice()).thenReturn(mSecondBluetoothDevice);
274
275        mController.updateState(mPreference);
276
277        assertThat(mPreference.isVisible()).isTrue();
278        assertThat(mPreference.getSummary()).isEqualTo(TEST_DEVICE_NAME_2);
279    }
280
281    /**
282     * Hands Free Profile Bluetooth device(s) are available, but wired headset is plugged in
283     * and activated.
284     * Preference should be visible
285     * Preference summary should be "This device"
286     */
287    @Test
288    public void updateState_withAvailableDevicesWiredHeadsetActivated_shouldSetDefaultSummary() {
289        mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
290        mShadowAudioManager.setOutputDevice(DEVICE_OUT_USB_HEADSET);
291        mProfileConnectedDevices.clear();
292        mProfileConnectedDevices.add(mBluetoothDevice);
293        when(mHeadsetProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices);
294        when(mHeadsetProfile.getActiveDevice()).thenReturn(
295                mBluetoothDevice); // BT device is still activated in this case
296
297        mController.updateState(mPreference);
298
299        assertThat(mPreference.isVisible()).isTrue();
300        assertThat(mPreference.getSummary()).isEqualTo(
301                mContext.getText(R.string.media_output_default_summary));
302    }
303
304    /**
305     * No available Headset BT devices
306     * Preference should be invisible
307     * Preference summary should be "This device"
308     */
309    @Test
310    public void updateState_noAvailableHeadsetBtDevices_shouldSetDefaultSummary() {
311        mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
312        List<BluetoothDevice> emptyDeviceList = new ArrayList<>();
313        when(mHeadsetProfile.getConnectedDevices()).thenReturn(emptyDeviceList);
314
315        mController.updateState(mPreference);
316
317        assertThat(mPreference.isVisible()).isFalse();
318        assertThat(mPreference.getSummary()).isEqualTo(
319                mContext.getText(R.string.media_output_default_summary));
320    }
321
322    /**
323     * One hearing aid profile Bluetooth device is available and active.
324     * Preference should be visible
325     * Preference summary should be the activated device name
326     */
327    @Test
328    public void updateState_oneHapBtDeviceAreAvailable_shouldSetActivatedDeviceName() {
329        mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
330        mShadowAudioManager.setOutputDevice(DEVICE_OUT_HEARING_AID);
331        mProfileConnectedDevices.clear();
332        mProfileConnectedDevices.add(mLeftBluetoothHapDevice);
333        mHearingAidActiveDevices.clear();
334        mHearingAidActiveDevices.add(mLeftBluetoothHapDevice);
335        when(mHearingAidProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices);
336        when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices);
337        when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1);
338
339        mController.updateState(mPreference);
340
341        assertThat(mPreference.isVisible()).isTrue();
342        assertThat(mPreference.getSummary()).isEqualTo(mLeftBluetoothHapDevice.getName());
343    }
344
345    /**
346     * More than one hearing aid profile Bluetooth devices are available, and second
347     * device is active.
348     * Preference should be visible
349     * Preference summary should be the activated device name
350     */
351    @Test
352    public void updateState_moreThanOneHapBtDevicesAreAvailable_shouldSetActivatedDeviceName() {
353        mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
354        mShadowAudioManager.setOutputDevice(DEVICE_OUT_HEARING_AID);
355        mProfileConnectedDevices.clear();
356        mProfileConnectedDevices.add(mLeftBluetoothHapDevice);
357        mProfileConnectedDevices.add(mRightBluetoothHapDevice);
358        mHearingAidActiveDevices.clear();
359        mHearingAidActiveDevices.add(mRightBluetoothHapDevice);
360        when(mHearingAidProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices);
361        when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices);
362        when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1);
363        when(mHearingAidProfile.getHiSyncId(mRightBluetoothHapDevice)).thenReturn(HISYNCID2);
364
365        mController.updateState(mPreference);
366
367        assertThat(mPreference.isVisible()).isTrue();
368        assertThat(mPreference.getSummary()).isEqualTo(mRightBluetoothHapDevice.getName());
369    }
370
371    /**
372     * Both hearing aid profile and hands free profile Bluetooth devices are available, and
373     * two hearing aid profile devices with same HisyncId. Both of HAP device are active,
374     * "left" side HAP device is added first.
375     * Preference should be visible
376     * Preference summary should be the activated device name
377     * ConnectedDevice should not contain second HAP device with same HisyncId
378     */
379    @Test
380    public void updateState_hapBtDeviceWithSameId_shouldSetActivatedDeviceName() {
381        mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
382        mShadowAudioManager.setOutputDevice(DEVICE_OUT_HEARING_AID);
383        mProfileConnectedDevices.clear();
384        mProfileConnectedDevices.add(mBluetoothDevice);
385        //with same HisyncId, only the first one will remain in UI.
386        mProfileConnectedDevices.add(mLeftBluetoothHapDevice);
387        mProfileConnectedDevices.add(mRightBluetoothHapDevice);
388        mHearingAidActiveDevices.clear();
389        mHearingAidActiveDevices.add(mLeftBluetoothHapDevice);
390        mHearingAidActiveDevices.add(mRightBluetoothHapDevice);
391        when(mHearingAidProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices);
392        when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices);
393        when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1);
394        when(mHearingAidProfile.getHiSyncId(mRightBluetoothHapDevice)).thenReturn(HISYNCID1);
395
396        mController.updateState(mPreference);
397
398        assertThat(mPreference.isVisible()).isTrue();
399        assertThat(mPreference.getSummary()).isEqualTo(mLeftBluetoothHapDevice.getName());
400        assertThat(mController.mConnectedDevices.contains(mLeftBluetoothHapDevice)).isTrue();
401        assertThat(mController.mConnectedDevices.contains(mRightBluetoothHapDevice)).isFalse();
402    }
403
404    /**
405     * Both hearing aid profile and hands free profile Bluetooth devices are available, and
406     * two hearing aid profile devices with same HisyncId. Both of HAP device are active,
407     * "right" side HAP device is added first.
408     * Preference should be visible
409     * Preference summary should be the activated device name
410     * ConnectedDevice should not contain second HAP device with same HisyncId
411     */
412    @Test
413    public void updateState_hapBtDeviceWithSameIdButDifferentOrder_shouldSetActivatedDeviceName() {
414        mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
415        mShadowAudioManager.setOutputDevice(DEVICE_OUT_HEARING_AID);
416        mProfileConnectedDevices.clear();
417        mProfileConnectedDevices.add(mBluetoothDevice);
418        //with same HisyncId, only the first one will remain in UI.
419        mProfileConnectedDevices.add(mRightBluetoothHapDevice);
420        mProfileConnectedDevices.add(mLeftBluetoothHapDevice);
421        mHearingAidActiveDevices.clear();
422        mHearingAidActiveDevices.add(mLeftBluetoothHapDevice);
423        mHearingAidActiveDevices.add(mRightBluetoothHapDevice);
424        when(mHearingAidProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices);
425        when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices);
426        when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1);
427        when(mHearingAidProfile.getHiSyncId(mRightBluetoothHapDevice)).thenReturn(HISYNCID1);
428
429        mController.updateState(mPreference);
430
431        assertThat(mPreference.isVisible()).isTrue();
432        assertThat(mController.mConnectedDevices.contains(mRightBluetoothHapDevice)).isTrue();
433        assertThat(mController.mConnectedDevices.contains(mLeftBluetoothHapDevice)).isFalse();
434        assertThat(mPreference.getSummary()).isEqualTo(mRightBluetoothHapDevice.getName());
435    }
436
437    /**
438     * Both hearing aid profile and hands free profile  Bluetooth devices are available, and
439     * two hearing aid profile devices with different HisyncId. One of HAP device is active.
440     * Preference should be visible
441     * Preference summary should be the activated device name
442     * ConnectedDevice should contain both HAP device with different HisyncId
443     */
444    @Test
445    public void updateState_hapBtDeviceWithDifferentId_shouldSetActivatedDeviceName() {
446        mShadowAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
447        mShadowAudioManager.setOutputDevice(DEVICE_OUT_HEARING_AID);
448        mProfileConnectedDevices.clear();
449        mProfileConnectedDevices.add(mBluetoothDevice);
450        mProfileConnectedDevices.add(mLeftBluetoothHapDevice);
451        mProfileConnectedDevices.add(mRightBluetoothHapDevice);
452        mHearingAidActiveDevices.clear();
453        mHearingAidActiveDevices.add(null);
454        mHearingAidActiveDevices.add(mRightBluetoothHapDevice);
455        when(mHearingAidProfile.getConnectedDevices()).thenReturn(mProfileConnectedDevices);
456        when(mHearingAidProfile.getActiveDevices()).thenReturn(mHearingAidActiveDevices);
457        when(mHearingAidProfile.getHiSyncId(mLeftBluetoothHapDevice)).thenReturn(HISYNCID1);
458        when(mHearingAidProfile.getHiSyncId(mRightBluetoothHapDevice)).thenReturn(HISYNCID2);
459
460        mController.updateState(mPreference);
461
462        assertThat(mPreference.isVisible()).isTrue();
463        assertThat(mPreference.getSummary()).isEqualTo(mRightBluetoothHapDevice.getName());
464        assertThat(mController.mConnectedDevices).containsExactly(mBluetoothDevice,
465                mLeftBluetoothHapDevice, mRightBluetoothHapDevice);
466    }
467}
468