1 /*
2  * Copyright (C) 2023 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.accessibility;
18 
19 
20 import static com.google.common.truth.Truth.assertThat;
21 
22 import static org.mockito.Mockito.doReturn;
23 import static org.mockito.Mockito.mock;
24 import static org.mockito.Mockito.never;
25 import static org.mockito.Mockito.verify;
26 
27 import android.bluetooth.BluetoothAdapter;
28 import android.bluetooth.BluetoothDevice;
29 import android.bluetooth.BluetoothProfile;
30 import android.bluetooth.BluetoothUuid;
31 import android.bluetooth.le.ScanRecord;
32 import android.bluetooth.le.ScanResult;
33 import android.content.Context;
34 import android.graphics.drawable.Drawable;
35 import android.util.Pair;
36 
37 import androidx.preference.Preference;
38 import androidx.test.core.app.ApplicationProvider;
39 
40 import com.android.settings.bluetooth.BluetoothDevicePreference;
41 import com.android.settings.bluetooth.BluetoothProgressCategory;
42 import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
43 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
44 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
45 import com.android.settingslib.bluetooth.HearingAidInfo;
46 import com.android.settingslib.bluetooth.LocalBluetoothManager;
47 
48 import org.junit.Before;
49 import org.junit.Rule;
50 import org.junit.Test;
51 import org.junit.runner.RunWith;
52 import org.mockito.Mock;
53 import org.mockito.Spy;
54 import org.mockito.junit.MockitoJUnit;
55 import org.mockito.junit.MockitoRule;
56 import org.robolectric.RobolectricTestRunner;
57 import org.robolectric.annotation.Config;
58 
59 import java.util.List;
60 
61 /** Tests for {@link HearingDevicePairingFragment}. */
62 @RunWith(RobolectricTestRunner.class)
63 @Config(shadows = {ShadowBluetoothAdapter.class})
64 public class HearingDevicePairingFragmentTest {
65 
66     private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1";
67 
68     @Rule
69     public final MockitoRule mockito = MockitoJUnit.rule();
70 
71     @Spy
72     private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
73     @Spy
74     private final HearingDevicePairingFragment mFragment = new TestHearingDevicePairingFragment();
75 
76     @Mock
77     private LocalBluetoothManager mLocalManager;
78     @Mock
79     private CachedBluetoothDeviceManager mCachedDeviceManager;
80     @Mock
81     private CachedBluetoothDevice mCachedDevice;
82     @Mock
83     private BluetoothProgressCategory mAvailableHearingDeviceGroup;
84 
85     private final Context mContext = ApplicationProvider.getApplicationContext();
86     private BluetoothDevice mDevice;
87     private BluetoothDevicePreference mDevicePreference;
88 
89 
90     @Before
setUp()91     public void setUp() {
92         mFragment.mLocalManager = mLocalManager;
93         mFragment.mCachedDeviceManager = mCachedDeviceManager;
94         mFragment.mBluetoothAdapter = mBluetoothAdapter;
95         doReturn(mContext).when(mFragment).getContext();
96         doReturn(mAvailableHearingDeviceGroup).when(mFragment).findPreference(
97                 "available_hearing_devices");
98         mFragment.initPreferencesFromPreferenceScreen();
99 
100 
101         mDevice = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS);
102         doReturn(mDevice).when(mCachedDevice).getDevice();
103         final Pair<Drawable, String> pair = new Pair<>(mock(Drawable.class), "test_device");
104         doReturn(pair).when(mCachedDevice).getDrawableWithDescription();
105 
106         mDevicePreference = new BluetoothDevicePreference(mContext, mCachedDevice, true,
107                 BluetoothDevicePreference.SortType.TYPE_DEFAULT);
108     }
109 
110     @Test
startAndStopScanning_stateIsCorrect()111     public void startAndStopScanning_stateIsCorrect() {
112         mFragment.startScanning();
113 
114         verify(mFragment).startLeScanning();
115 
116         mFragment.stopScanning();
117 
118         verify(mFragment).stopLeScanning();
119     }
120 
121     @Test
onDeviceDeleted_stateIsCorrect()122     public void onDeviceDeleted_stateIsCorrect() {
123         mFragment.mDevicePreferenceMap.put(mCachedDevice, mDevicePreference);
124 
125         assertThat(mFragment.mDevicePreferenceMap).isNotEmpty();
126 
127         mFragment.onDeviceDeleted(mCachedDevice);
128 
129         assertThat(mFragment.mDevicePreferenceMap).isEmpty();
130         verify(mAvailableHearingDeviceGroup).removePreference(mDevicePreference);
131     }
132 
133     @Test
addDevice_bluetoothOff_doNothing()134     public void addDevice_bluetoothOff_doNothing() {
135         doReturn(BluetoothAdapter.STATE_OFF).when(mBluetoothAdapter).getState();
136 
137         assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(0);
138 
139         mFragment.addDevice(mCachedDevice);
140 
141         verify(mAvailableHearingDeviceGroup, never()).addPreference(mDevicePreference);
142         assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(0);
143     }
144 
145     @Test
addDevice_addToAvailableHearingDeviceGroup()146     public void addDevice_addToAvailableHearingDeviceGroup() {
147         doReturn(BluetoothAdapter.STATE_ON).when(mBluetoothAdapter).getState();
148 
149         assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(0);
150 
151         mFragment.addDevice(mCachedDevice);
152 
153         verify(mAvailableHearingDeviceGroup).addPreference(mDevicePreference);
154         assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(1);
155     }
156 
157     @Test
handleLeScanResult_markDeviceAsHearingAid()158     public void handleLeScanResult_markDeviceAsHearingAid() {
159         ScanResult scanResult = mock(ScanResult.class);
160         doReturn(mDevice).when(scanResult).getDevice();
161         doReturn(mCachedDevice).when(mCachedDeviceManager).findDevice(mDevice);
162 
163         mFragment.handleLeScanResult(scanResult);
164 
165         verify(mCachedDevice).setHearingAidInfo(new HearingAidInfo.Builder().build());
166     }
167 
168     @Test
handleLeScanResult_isAndroidCompatible_addDevice()169     public void handleLeScanResult_isAndroidCompatible_addDevice() {
170         ScanResult scanResult = mock(ScanResult.class);
171         doReturn(mDevice).when(scanResult).getDevice();
172         doReturn(mCachedDevice).when(mCachedDeviceManager).findDevice(mDevice);
173         doReturn(true).when(mFragment).isAndroidCompatibleHearingAid(scanResult);
174 
175         mFragment.handleLeScanResult(scanResult);
176 
177         verify(mFragment).addDevice(mCachedDevice);
178     }
179 
180     @Test
handleLeScanResult_isNotAndroidCompatible_discoverServices()181     public void handleLeScanResult_isNotAndroidCompatible_discoverServices() {
182         ScanResult scanResult = mock(ScanResult.class);
183         doReturn(mDevice).when(scanResult).getDevice();
184         doReturn(mCachedDevice).when(mCachedDeviceManager).findDevice(mDevice);
185         doReturn(false).when(mFragment).isAndroidCompatibleHearingAid(scanResult);
186 
187         mFragment.handleLeScanResult(scanResult);
188 
189         verify(mFragment).discoverServices(mCachedDevice);
190     }
191 
192     @Test
handleLeScanResult_alreadyBonded_doNothing()193     public void handleLeScanResult_alreadyBonded_doNothing() {
194         ScanResult scanResult = mock(ScanResult.class);
195         doReturn(mDevice).when(scanResult).getDevice();
196         doReturn(mCachedDevice).when(mCachedDeviceManager).findDevice(mDevice);
197         doReturn(BluetoothDevice.BOND_BONDED).when(mCachedDevice).getBondState();
198 
199         mFragment.handleLeScanResult(scanResult);
200 
201         verify(mFragment, never()).addDevice(mCachedDevice);
202         verify(mFragment, never()).discoverServices(mCachedDevice);
203     }
204 
205     @Test
onProfileConnectionStateChanged_deviceConnected_inSelectedList_finish()206     public void onProfileConnectionStateChanged_deviceConnected_inSelectedList_finish() {
207         doReturn(true).when(mCachedDevice).isConnected();
208         mFragment.mSelectedDeviceList.add(mDevice);
209 
210         mFragment.onProfileConnectionStateChanged(mCachedDevice, BluetoothAdapter.STATE_CONNECTED,
211                 BluetoothProfile.A2DP);
212 
213         verify(mFragment).finish();
214     }
215 
216     @Test
onProfileConnectionStateChanged_deviceConnected_notInSelectedList_deleteDevice()217     public void onProfileConnectionStateChanged_deviceConnected_notInSelectedList_deleteDevice() {
218         doReturn(true).when(mCachedDevice).isConnected();
219 
220         mFragment.onProfileConnectionStateChanged(mCachedDevice, BluetoothAdapter.STATE_CONNECTED,
221                 BluetoothProfile.A2DP);
222 
223         verify(mFragment).removeDevice(mCachedDevice);
224     }
225 
226     @Test
onProfileConnectionStateChanged_deviceNotConnected_doNothing()227     public void onProfileConnectionStateChanged_deviceNotConnected_doNothing() {
228         doReturn(false).when(mCachedDevice).isConnected();
229 
230         mFragment.onProfileConnectionStateChanged(mCachedDevice, BluetoothAdapter.STATE_CONNECTED,
231                 BluetoothProfile.A2DP);
232 
233         verify(mFragment, never()).finish();
234         verify(mFragment, never()).removeDevice(mCachedDevice);
235     }
236 
237     @Test
onBluetoothStateChanged_stateOn_startScanningAndShowToast()238     public void onBluetoothStateChanged_stateOn_startScanningAndShowToast() {
239         mFragment.onBluetoothStateChanged(BluetoothAdapter.STATE_ON);
240 
241         verify(mFragment).startScanning();
242         verify(mFragment).showBluetoothTurnedOnToast();
243     }
244 
245     @Test
onBluetoothStateChanged_stateOff_finish()246     public void onBluetoothStateChanged_stateOff_finish() {
247         mFragment.onBluetoothStateChanged(BluetoothAdapter.STATE_OFF);
248 
249         verify(mFragment).finish();
250     }
251 
252     @Test
onDeviceBondStateChanged_bonded_finish()253     public void onDeviceBondStateChanged_bonded_finish() {
254         mFragment.onDeviceBondStateChanged(mCachedDevice, BluetoothDevice.BOND_BONDED);
255 
256         verify(mFragment).finish();
257     }
258 
259     @Test
onDeviceBondStateChanged_selectedDeviceNotBonded_startScanning()260     public void onDeviceBondStateChanged_selectedDeviceNotBonded_startScanning() {
261         mFragment.mSelectedDevice = mDevice;
262 
263         mFragment.onDeviceBondStateChanged(mCachedDevice, BluetoothDevice.BOND_NONE);
264 
265         verify(mFragment).startScanning();
266     }
267 
268     @Test
isAndroidCompatibleHearingAid_asha_returnTrue()269     public void isAndroidCompatibleHearingAid_asha_returnTrue() {
270         ScanResult scanResult = createAshaScanResult();
271 
272         boolean isCompatible = mFragment.isAndroidCompatibleHearingAid(scanResult);
273 
274         assertThat(isCompatible).isTrue();
275     }
276 
277     @Test
isAndroidCompatibleHearingAid_has_returnTrue()278     public void isAndroidCompatibleHearingAid_has_returnTrue() {
279         ScanResult scanResult = createHasScanResult();
280 
281         boolean isCompatible = mFragment.isAndroidCompatibleHearingAid(scanResult);
282 
283         assertThat(isCompatible).isTrue();
284     }
285 
286     @Test
isAndroidCompatibleHearingAid_mfiHas_returnFalse()287     public void isAndroidCompatibleHearingAid_mfiHas_returnFalse() {
288         ScanResult scanResult = createMfiHasScanResult();
289 
290         boolean isCompatible = mFragment.isAndroidCompatibleHearingAid(scanResult);
291 
292         assertThat(isCompatible).isFalse();
293     }
294 
createAshaScanResult()295     private ScanResult createAshaScanResult() {
296         ScanResult scanResult = mock(ScanResult.class);
297         ScanRecord scanRecord = mock(ScanRecord.class);
298         byte[] fakeAshaServiceData = new byte[] {
299                 0x09, 0x16, (byte) 0xf0, (byte) 0xfd, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04};
300         doReturn(scanRecord).when(scanResult).getScanRecord();
301         doReturn(fakeAshaServiceData).when(scanRecord).getServiceData(BluetoothUuid.HEARING_AID);
302         return scanResult;
303     }
304 
createHasScanResult()305     private ScanResult createHasScanResult() {
306         ScanResult scanResult = mock(ScanResult.class);
307         ScanRecord scanRecord = mock(ScanRecord.class);
308         doReturn(scanRecord).when(scanResult).getScanRecord();
309         doReturn(List.of(BluetoothUuid.HAS)).when(scanRecord).getServiceUuids();
310         return scanResult;
311     }
312 
createMfiHasScanResult()313     private ScanResult createMfiHasScanResult() {
314         ScanResult scanResult = mock(ScanResult.class);
315         ScanRecord scanRecord = mock(ScanRecord.class);
316         byte[] fakeMfiServiceData = new byte[] {0x00, 0x00, 0x00, 0x00};
317         doReturn(scanRecord).when(scanResult).getScanRecord();
318         doReturn(fakeMfiServiceData).when(scanRecord).getServiceData(BluetoothUuid.MFI_HAS);
319         return scanResult;
320     }
321 
322     private class TestHearingDevicePairingFragment extends HearingDevicePairingFragment {
323         @Override
getCachedPreference(String key)324         protected Preference getCachedPreference(String key) {
325             if (key.equals(TEST_DEVICE_ADDRESS)) {
326                 return mDevicePreference;
327             }
328             return super.getCachedPreference(key);
329         }
330     }
331 }
332