1 /*
2  * Copyright (C) 2021 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.car.settings.qc;
18 
19 import static android.os.UserManager.DISALLOW_BLUETOOTH;
20 
21 import static com.android.car.qc.QCItem.QC_ACTION_TOGGLE_STATE;
22 import static com.android.car.settings.qc.PairedBluetoothDevices.BLUETOOTH_BUTTON;
23 import static com.android.car.settings.qc.PairedBluetoothDevices.EXTRA_BUTTON_TYPE;
24 import static com.android.car.settings.qc.PairedBluetoothDevices.EXTRA_DEVICE_KEY;
25 import static com.android.car.settings.qc.PairedBluetoothDevices.MEDIA_BUTTON;
26 import static com.android.car.settings.qc.PairedBluetoothDevices.PHONE_BUTTON;
27 
28 import static com.google.common.truth.Truth.assertThat;
29 
30 import static org.mockito.ArgumentMatchers.any;
31 import static org.mockito.ArgumentMatchers.eq;
32 import static org.mockito.Mockito.mock;
33 import static org.mockito.Mockito.verify;
34 import static org.mockito.Mockito.when;
35 
36 import android.bluetooth.BluetoothAdapter;
37 import android.bluetooth.BluetoothClass;
38 import android.bluetooth.BluetoothDevice;
39 import android.bluetooth.BluetoothProfile;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.os.UserManager;
43 
44 import androidx.test.core.app.ApplicationProvider;
45 import androidx.test.ext.junit.runners.AndroidJUnit4;
46 
47 import com.android.car.qc.QCActionItem;
48 import com.android.car.qc.QCItem;
49 import com.android.car.qc.QCList;
50 import com.android.car.qc.QCRow;
51 import com.android.car.settings.R;
52 import com.android.dx.mockito.inline.extended.ExtendedMockito;
53 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
54 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
55 import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
56 import com.android.settingslib.bluetooth.LocalBluetoothManager;
57 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
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.mockito.MockitoSession;
66 import org.mockito.quality.Strictness;
67 
68 import java.util.ArrayList;
69 import java.util.Arrays;
70 import java.util.HashSet;
71 import java.util.Set;
72 
73 @RunWith(AndroidJUnit4.class)
74 public class PairedBluetoothDevicesTest {
75     private static final String DEFAULT_NAME = "DEFAULT_NAME";
76     private static final String DEFAULT_SUMMARY = "DEFAULT_SUMMARY";
77     private static final String DEFAULT_ADDRESS = "F6:8F:AC:E8:32:50";
78 
79     private Context mContext = ApplicationProvider.getApplicationContext();
80     private PairedBluetoothDevices mPairedBluetoothDevices;
81     private MockitoSession mSession;
82     private ArrayList<CachedBluetoothDevice> mCachedDevices = new ArrayList<>();
83     private Set<BluetoothDevice> mBondedDevices = new HashSet<>();
84 
85     @Mock
86     private BluetoothAdapter mBluetoothAdapter;
87     @Mock
88     private LocalBluetoothAdapter mLocalBluetoothAdapter;
89     @Mock
90     private LocalBluetoothManager mBluetoothManager;
91     @Mock
92     private UserManager mUserManager;
93 
94     @Before
setUp()95     public void setUp() {
96         MockitoAnnotations.initMocks(this);
97         mSession = ExtendedMockito.mockitoSession()
98                 .strictness(Strictness.LENIENT)
99                 .mockStatic(BluetoothAdapter.class)
100                 .mockStatic(LocalBluetoothManager.class)
101                 .mockStatic(UserManager.class)
102                 .startMocking();
103         when(BluetoothAdapter.getDefaultAdapter()).thenReturn(mBluetoothAdapter);
104         when(mBluetoothAdapter.isEnabled()).thenReturn(true);
105         when(LocalBluetoothManager.getInstance(any(), any())).thenReturn(mBluetoothManager);
106         when(mBluetoothManager.getBluetoothAdapter()).thenReturn(mLocalBluetoothAdapter);
107         when(mLocalBluetoothAdapter.getBondedDevices()).thenReturn(mBondedDevices);
108         when(UserManager.get(any())).thenReturn(mUserManager);
109         when(mUserManager.hasUserRestriction(DISALLOW_BLUETOOTH)).thenReturn(false);
110 
111         CachedBluetoothDeviceManager deviceManager = mock(CachedBluetoothDeviceManager.class);
112         when(mBluetoothManager.getCachedDeviceManager()).thenReturn(deviceManager);
113         when(deviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices);
114 
115         mPairedBluetoothDevices = new PairedBluetoothDevices(mContext);
116     }
117 
118     @After
tearDown()119     public void tearDown() {
120         if (mSession != null) {
121             mSession.finishMocking();
122         }
123     }
124 
125     @Test
getQCItem_bluetoothDisallowed_returnsNull()126     public void getQCItem_bluetoothDisallowed_returnsNull() {
127         when(mUserManager.hasUserRestriction(DISALLOW_BLUETOOTH)).thenReturn(true);
128         QCItem item = mPairedBluetoothDevices.getQCItem();
129         assertThat(item).isNull();
130     }
131 
132     @Test
getQCItem_bluetoothDisabled_returnsBluetoothDisabledMessage()133     public void getQCItem_bluetoothDisabled_returnsBluetoothDisabledMessage() {
134         when(mBluetoothAdapter.isEnabled()).thenReturn(false);
135         QCItem item = mPairedBluetoothDevices.getQCItem();
136         assertThat(item).isNotNull();
137         QCList list = (QCList) item;
138         assertThat(list.getRows().size()).isEqualTo(1);
139         QCRow row = list.getRows().get(0);
140         assertThat(row.getTitle()).isEqualTo(
141                 mContext.getString(R.string.qc_bluetooth_off_devices_info));
142         assertThat(row.getStartIcon()).isNotNull();
143     }
144 
145     @Test
getQCItem_noDevices_returnsPairMessage()146     public void getQCItem_noDevices_returnsPairMessage() {
147         QCList list = (QCList) mPairedBluetoothDevices.getQCItem();
148         assertThat(list.getRows().size()).isEqualTo(1);
149         QCRow row = list.getRows().get(0);
150         assertThat(row.getTitle()).isEqualTo(
151                 mContext.getString(R.string.qc_bluetooth_on_no_devices_info));
152         assertThat(row.getStartIcon()).isNotNull();
153     }
154 
155     @Test
getQCItem_hasDevices_hasRows()156     public void getQCItem_hasDevices_hasRows() {
157         addBluetoothDevice("Device1", /* connected= */ true, /* busy= */ false,
158                 /* phoneEnabled= */ true, /* mediaEnabled= */ true);
159         addBluetoothDevice("Device2",  /* connected= */ true, /* busy= */ false,
160                 /* phoneEnabled= */ true, /* mediaEnabled= */ true);
161         QCList list = (QCList) mPairedBluetoothDevices.getQCItem();
162         assertThat(list.getRows().size()).isEqualTo(2);
163     }
164 
165     @Test
getQCItem_limitsDeviceCount()166     public void getQCItem_limitsDeviceCount() {
167         addBluetoothDevice("Device1", /* connected= */ true, /* busy= */ false,
168                 /* phoneEnabled= */ true, /* mediaEnabled= */ true);
169         addBluetoothDevice("Device2",  /* connected= */ false, /* busy= */ false,
170                 /* phoneEnabled= */ false, /* mediaEnabled= */ false);
171         addBluetoothDevice("Device3",  /* connected= */ false, /* busy= */ false,
172                 /* phoneEnabled= */ false, /* mediaEnabled= */ false);
173         addBluetoothDevice("Device4",  /* connected= */ false, /* busy= */ false,
174                 /* phoneEnabled= */ false, /* mediaEnabled= */ false);
175         QCList list = (QCList) mPairedBluetoothDevices.getQCItem();
176         assertThat(list.getRows().size()).isEqualTo(3);
177     }
178 
179     @Test
getQCItem_setsTitle()180     public void getQCItem_setsTitle() {
181         addBluetoothDevice(DEFAULT_NAME, /* connected= */ true, /* busy= */ false,
182                 /* phoneEnabled= */ true, /* mediaEnabled= */ true);
183         QCList list = (QCList) mPairedBluetoothDevices.getQCItem();
184         QCRow row = list.getRows().get(0);
185         assertThat(row.getTitle()).isEqualTo(DEFAULT_NAME);
186     }
187 
188     @Test
getQCItem_bluetoothDisabled_togglesUpdated()189     public void getQCItem_bluetoothDisabled_togglesUpdated() {
190         addBluetoothDevice(DEFAULT_NAME, /* connected= */ false, /* busy= */ false,
191                 /* phoneEnabled= */ true, /* mediaEnabled= */ true);
192         QCList list = (QCList) mPairedBluetoothDevices.getQCItem();
193         QCRow row = list.getRows().get(0);
194         QCActionItem btToggle = row.getEndItems().get(0);
195         QCActionItem phoneToggle = row.getEndItems().get(1);
196         QCActionItem mediaToggle = row.getEndItems().get(2);
197         assertThat(btToggle.isChecked()).isFalse();
198         assertThat(btToggle.isEnabled()).isTrue();
199         assertThat(phoneToggle.isAvailable()).isFalse();
200         assertThat(mediaToggle.isAvailable()).isFalse();
201     }
202 
203     @Test
getQCItem_bluetoothEnabled_togglesUpdated()204     public void getQCItem_bluetoothEnabled_togglesUpdated() {
205         addBluetoothDevice(DEFAULT_NAME, /* connected= */ true, /* busy= */ false,
206                 /* phoneEnabled= */ true, /* mediaEnabled= */ true);
207         QCList list = (QCList) mPairedBluetoothDevices.getQCItem();
208         QCRow row = list.getRows().get(0);
209         QCActionItem btToggle = row.getEndItems().get(0);
210         QCActionItem phoneToggle = row.getEndItems().get(1);
211         QCActionItem mediaToggle = row.getEndItems().get(2);
212         assertThat(btToggle.isChecked()).isTrue();
213         assertThat(btToggle.isEnabled()).isTrue();
214         assertThat(phoneToggle.isChecked()).isTrue();
215         assertThat(phoneToggle.isEnabled()).isTrue();
216         assertThat(phoneToggle.isAvailable()).isTrue();
217         assertThat(mediaToggle.isChecked()).isTrue();
218         assertThat(mediaToggle.isEnabled()).isTrue();
219         assertThat(mediaToggle.isAvailable()).isTrue();
220     }
221 
222     @Test
getQCItem_isBusy_togglesDisabled()223     public void getQCItem_isBusy_togglesDisabled() {
224         addBluetoothDevice(DEFAULT_NAME, /* connected= */ true, /* busy= */ true,
225                 /* phoneEnabled= */ true, /* mediaEnabled= */ true);
226         QCList list = (QCList) mPairedBluetoothDevices.getQCItem();
227         QCRow row = list.getRows().get(0);
228         QCActionItem btToggle = row.getEndItems().get(0);
229         QCActionItem phoneToggle = row.getEndItems().get(1);
230         QCActionItem mediaToggle = row.getEndItems().get(2);
231         assertThat(btToggle.isEnabled()).isFalse();
232         assertThat(phoneToggle.isEnabled()).isFalse();
233         assertThat(mediaToggle.isEnabled()).isFalse();
234     }
235 
236     @Test
onNotifyChange_toggleBluetooth()237     public void onNotifyChange_toggleBluetooth() {
238         addBluetoothDevice(DEFAULT_NAME, /* connected= */ false, /* busy= */ true,
239                 /* phoneEnabled= */ true, /* mediaEnabled= */ true);
240         Intent intent = new Intent();
241         intent.putExtra(EXTRA_DEVICE_KEY, DEFAULT_ADDRESS);
242         intent.putExtra(EXTRA_BUTTON_TYPE, BLUETOOTH_BUTTON);
243         intent.putExtra(QC_ACTION_TOGGLE_STATE, true);
244         mPairedBluetoothDevices.onNotifyChange(intent);
245         CachedBluetoothDevice device = mCachedDevices.get(0);
246         verify(device).connect();
247     }
248 
249     @Test
onNotifyChange_togglePhone()250     public void onNotifyChange_togglePhone() {
251         addBluetoothDevice(DEFAULT_NAME, /* connected= */ false, /* busy= */ true,
252                 /* phoneEnabled= */ false, /* mediaEnabled= */ true);
253         Intent intent = new Intent();
254         intent.putExtra(EXTRA_DEVICE_KEY, DEFAULT_ADDRESS);
255         intent.putExtra(EXTRA_BUTTON_TYPE, PHONE_BUTTON);
256         intent.putExtra(QC_ACTION_TOGGLE_STATE, true);
257         mPairedBluetoothDevices.onNotifyChange(intent);
258         CachedBluetoothDevice device = mCachedDevices.get(0);
259         LocalBluetoothProfile profile = getProfile(device, BluetoothProfile.HEADSET_CLIENT);
260         verify(profile).setEnabled(any(), eq(true));
261     }
262 
263     @Test
onNotifyChange_toggleMedia()264     public void onNotifyChange_toggleMedia() {
265         addBluetoothDevice(DEFAULT_NAME, /* connected= */ false, /* busy= */ true,
266                 /* phoneEnabled= */ true, /* mediaEnabled= */ false);
267         Intent intent = new Intent();
268         intent.putExtra(EXTRA_DEVICE_KEY, DEFAULT_ADDRESS);
269         intent.putExtra(EXTRA_BUTTON_TYPE, MEDIA_BUTTON);
270         intent.putExtra(QC_ACTION_TOGGLE_STATE, true);
271         mPairedBluetoothDevices.onNotifyChange(intent);
272         CachedBluetoothDevice device = mCachedDevices.get(0);
273         LocalBluetoothProfile profile = getProfile(device, BluetoothProfile.A2DP_SINK);
274         verify(profile).setEnabled(any(), eq(true));
275     }
276 
addBluetoothDevice(String name, boolean connected, boolean busy, boolean phoneEnabled, boolean mediaEnabled)277     private void addBluetoothDevice(String name, boolean connected, boolean busy,
278             boolean phoneEnabled, boolean mediaEnabled) {
279         CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class);
280         BluetoothDevice device = mock(BluetoothDevice.class);
281         when(cachedDevice.getDevice()).thenReturn(device);
282         when(device.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
283         when(cachedDevice.getName()).thenReturn(name);
284         when(cachedDevice.getAddress()).thenReturn(DEFAULT_ADDRESS);
285         when(cachedDevice.isConnected()).thenReturn(connected);
286         when(cachedDevice.isBusy()).thenReturn(busy);
287         when(cachedDevice.getCarConnectionSummary()).thenReturn(DEFAULT_SUMMARY);
288         BluetoothClass btClass = mock(BluetoothClass.class);
289         when(cachedDevice.getBtClass()).thenReturn(btClass);
290         when(btClass.getMajorDeviceClass()).thenReturn(BluetoothClass.Device.Major.PHONE);
291         LocalBluetoothProfile phoneProfile = mock(LocalBluetoothProfile.class);
292         LocalBluetoothProfile mediaProfile = mock(LocalBluetoothProfile.class);
293         when(phoneProfile.getProfileId()).thenReturn(BluetoothProfile.HEADSET_CLIENT);
294         when(mediaProfile.getProfileId()).thenReturn(BluetoothProfile.A2DP_SINK);
295         when(phoneProfile.isEnabled(any())).thenReturn(phoneEnabled);
296         when(mediaProfile.isEnabled(any())).thenReturn(mediaEnabled);
297         when(cachedDevice.getProfiles()).thenReturn(Arrays.asList(phoneProfile, mediaProfile));
298         mCachedDevices.add(cachedDevice);
299         mBondedDevices.add(device);
300     }
301 
getProfile(CachedBluetoothDevice device, int profileId)302     private LocalBluetoothProfile getProfile(CachedBluetoothDevice device, int profileId) {
303         for (LocalBluetoothProfile profile : device.getProfiles()) {
304             if (profile.getProfileId() == profileId) {
305                 return profile;
306             }
307         }
308         return null;
309     }
310 }
311