1 /*
2  * Copyright (C) 2017 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 com.google.common.truth.Truth.assertThat;
20 
21 import static org.mockito.ArgumentMatchers.eq;
22 import static org.mockito.Mockito.mock;
23 import static org.mockito.Mockito.spy;
24 import static org.mockito.Mockito.verify;
25 import static org.mockito.Mockito.when;
26 
27 import android.bluetooth.BluetoothClass;
28 import android.bluetooth.BluetoothDevice;
29 import android.bluetooth.BluetoothProfile;
30 import android.content.Context;
31 
32 import androidx.preference.Preference;
33 import androidx.preference.PreferenceCategory;
34 import androidx.preference.SwitchPreference;
35 
36 import com.android.settings.R;
37 import com.android.settings.testutils.shadow.ShadowBluetoothDevice;
38 import com.android.settingslib.bluetooth.A2dpProfile;
39 import com.android.settingslib.bluetooth.LocalBluetoothManager;
40 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
41 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
42 import com.android.settingslib.bluetooth.MapProfile;
43 import com.android.settingslib.bluetooth.PbapServerProfile;
44 
45 import org.junit.Test;
46 import org.junit.runner.RunWith;
47 import org.mockito.Mock;
48 import org.robolectric.RobolectricTestRunner;
49 import org.robolectric.annotation.Config;
50 
51 import java.util.ArrayList;
52 import java.util.HashMap;
53 import java.util.HashSet;
54 import java.util.List;
55 import java.util.Map;
56 import java.util.Set;
57 
58 @RunWith(RobolectricTestRunner.class)
59 @Config(shadows = ShadowBluetoothDevice.class)
60 public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsControllerTestBase {
61 
62     private BluetoothDetailsProfilesController mController;
63     private List<LocalBluetoothProfile> mConnectableProfiles;
64     private PreferenceCategory mProfiles;
65 
66     @Mock
67     private LocalBluetoothManager mLocalManager;
68     @Mock
69     private LocalBluetoothProfileManager mProfileManager;
70 
71     @Override
setUp()72     public void setUp() {
73         super.setUp();
74 
75         mProfiles = spy(new PreferenceCategory(mContext));
76         when(mProfiles.getPreferenceManager()).thenReturn(mPreferenceManager);
77 
78         mConnectableProfiles = new ArrayList<>();
79         when(mLocalManager.getProfileManager()).thenReturn(mProfileManager);
80         when(mCachedDevice.getConnectableProfiles()).thenAnswer(invocation ->
81             new ArrayList<>(mConnectableProfiles)
82         );
83 
84         setupDevice(mDeviceConfig);
85         mController = new BluetoothDetailsProfilesController(mContext, mFragment, mLocalManager,
86                 mCachedDevice, mLifecycle);
87         mProfiles.setKey(mController.getPreferenceKey());
88         mController.mProfilesContainer = mProfiles;
89         mScreen.addPreference(mProfiles);
90     }
91 
92     static class FakeBluetoothProfile implements LocalBluetoothProfile {
93 
94         private Set<BluetoothDevice> mConnectedDevices = new HashSet<>();
95         private Map<BluetoothDevice, Boolean> mPreferred = new HashMap<>();
96         private Context mContext;
97         private int mNameResourceId;
98 
FakeBluetoothProfile(Context context, int nameResourceId)99         private FakeBluetoothProfile(Context context, int nameResourceId) {
100             mContext = context;
101             mNameResourceId = nameResourceId;
102         }
103 
104         @Override
toString()105         public String toString() {
106             return mContext.getString(mNameResourceId);
107         }
108 
109         @Override
accessProfileEnabled()110         public boolean accessProfileEnabled() {
111             return true;
112         }
113 
114         @Override
isAutoConnectable()115         public boolean isAutoConnectable() {
116             return true;
117         }
118 
119         @Override
getConnectionStatus(BluetoothDevice device)120         public int getConnectionStatus(BluetoothDevice device) {
121             if (mConnectedDevices.contains(device)) {
122                 return BluetoothProfile.STATE_CONNECTED;
123             } else {
124                 return BluetoothProfile.STATE_DISCONNECTED;
125             }
126         }
127 
128         @Override
isEnabled(BluetoothDevice device)129         public boolean isEnabled(BluetoothDevice device) {
130             return mPreferred.getOrDefault(device, false);
131         }
132 
133         @Override
getConnectionPolicy(BluetoothDevice device)134         public int getConnectionPolicy(BluetoothDevice device) {
135             return isEnabled(device)
136                     ? BluetoothProfile.CONNECTION_POLICY_ALLOWED
137                     : BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
138         }
139 
140         @Override
setEnabled(BluetoothDevice device, boolean enabled)141         public boolean setEnabled(BluetoothDevice device, boolean enabled) {
142             mPreferred.put(device, enabled);
143             return true;
144         }
145 
146         @Override
isProfileReady()147         public boolean isProfileReady() {
148             return true;
149         }
150 
151         @Override
getProfileId()152         public int getProfileId() {
153             return 0;
154         }
155 
156         @Override
getOrdinal()157         public int getOrdinal() {
158             return 0;
159         }
160 
161         @Override
getNameResource(BluetoothDevice device)162         public int getNameResource(BluetoothDevice device) {
163             return mNameResourceId;
164         }
165 
166         @Override
getSummaryResourceForDevice(BluetoothDevice device)167         public int getSummaryResourceForDevice(BluetoothDevice device) {
168             return Utils.getConnectionStateSummary(getConnectionStatus(device));
169         }
170 
171         @Override
getDrawableResource(BluetoothClass btClass)172         public int getDrawableResource(BluetoothClass btClass) {
173             return 0;
174         }
175     }
176 
177     /**
178      * Creates and adds a mock LocalBluetoothProfile to the list of connectable profiles for the
179      * device.
180      * @param profileNameResId  the resource id for the name used by this profile
181      * @param deviceIsPreferred  whether this profile should start out as enabled for the device
182      */
addFakeProfile(int profileNameResId, boolean deviceIsPreferred)183     private LocalBluetoothProfile addFakeProfile(int profileNameResId,
184             boolean deviceIsPreferred) {
185         LocalBluetoothProfile profile = new FakeBluetoothProfile(mContext, profileNameResId);
186         profile.setEnabled(mDevice, deviceIsPreferred);
187         mConnectableProfiles.add(profile);
188         when(mProfileManager.getProfileByName(eq(profile.toString()))).thenReturn(profile);
189         return profile;
190     }
191 
192     /** Returns the list of SwitchPreference objects added to the screen - there should be one per
193      *  Bluetooth profile.
194      */
getProfileSwitches(boolean expectOnlyMConnectable)195     private List<SwitchPreference> getProfileSwitches(boolean expectOnlyMConnectable) {
196         if (expectOnlyMConnectable) {
197             assertThat(mConnectableProfiles).isNotEmpty();
198             assertThat(mProfiles.getPreferenceCount() - 1).isEqualTo(mConnectableProfiles.size());
199         }
200         List<SwitchPreference> result = new ArrayList<>();
201         for (int i = 0; i < mProfiles.getPreferenceCount(); i++) {
202             final Preference preference = mProfiles.getPreference(i);
203             if (preference instanceof SwitchPreference) {
204                 result.add((SwitchPreference) preference);
205             }
206         }
207         return result;
208     }
209 
verifyProfileSwitchTitles(List<SwitchPreference> switches)210      private void verifyProfileSwitchTitles(List<SwitchPreference> switches) {
211         for (int i = 0; i < switches.size(); i++) {
212             String expectedTitle =
213                 mContext.getString(mConnectableProfiles.get(i).getNameResource(mDevice));
214             assertThat(switches.get(i).getTitle()).isEqualTo(expectedTitle);
215         }
216     }
217 
218     @Test
oneProfile()219     public void oneProfile() {
220         addFakeProfile(R.string.bluetooth_profile_a2dp, true);
221         showScreen(mController);
222         verifyProfileSwitchTitles(getProfileSwitches(true));
223     }
224 
225     @Test
multipleProfiles()226     public void multipleProfiles() {
227         addFakeProfile(R.string.bluetooth_profile_a2dp, true);
228         addFakeProfile(R.string.bluetooth_profile_headset, false);
229         showScreen(mController);
230         List<SwitchPreference> switches = getProfileSwitches(true);
231         verifyProfileSwitchTitles(switches);
232         assertThat(switches.get(0).isChecked()).isTrue();
233         assertThat(switches.get(1).isChecked()).isFalse();
234 
235         // Both switches should be enabled.
236         assertThat(switches.get(0).isEnabled()).isTrue();
237         assertThat(switches.get(1).isEnabled()).isTrue();
238 
239         // Make device busy.
240         when(mCachedDevice.isBusy()).thenReturn(true);
241         mController.onDeviceAttributesChanged();
242 
243         // There should have been no new switches added.
244         assertThat(mProfiles.getPreferenceCount()).isEqualTo(3);
245 
246         // Make sure both switches got disabled.
247         assertThat(switches.get(0).isEnabled()).isFalse();
248         assertThat(switches.get(1).isEnabled()).isFalse();
249     }
250 
251     @Test
disableThenReenableOneProfile()252     public void disableThenReenableOneProfile() {
253         addFakeProfile(R.string.bluetooth_profile_a2dp, true);
254         addFakeProfile(R.string.bluetooth_profile_headset, true);
255         showScreen(mController);
256         List<SwitchPreference> switches = getProfileSwitches(true);
257         SwitchPreference pref = switches.get(0);
258 
259         // Clicking the pref should cause the profile to become not-preferred.
260         assertThat(pref.isChecked()).isTrue();
261         pref.performClick();
262         assertThat(pref.isChecked()).isFalse();
263         assertThat(mConnectableProfiles.get(0).isEnabled(mDevice)).isFalse();
264 
265         // Make sure no new preferences were added.
266         assertThat(mProfiles.getPreferenceCount()).isEqualTo(3);
267 
268         // Clicking the pref again should make the profile once again preferred.
269         pref.performClick();
270         assertThat(pref.isChecked()).isTrue();
271         assertThat(mConnectableProfiles.get(0).isEnabled(mDevice)).isTrue();
272 
273         // Make sure we still haven't gotten any new preferences added.
274         assertThat(mProfiles.getPreferenceCount()).isEqualTo(3);
275     }
276 
277     @Test
disconnectedDeviceOneProfile()278     public void disconnectedDeviceOneProfile() {
279         setupDevice(makeDefaultDeviceConfig().setConnected(false).setConnectionSummary(null));
280         addFakeProfile(R.string.bluetooth_profile_a2dp, true);
281         showScreen(mController);
282         verifyProfileSwitchTitles(getProfileSwitches(true));
283     }
284 
285     @Test
pbapProfileStartsEnabled()286     public void pbapProfileStartsEnabled() {
287         setupDevice(makeDefaultDeviceConfig());
288         mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
289         PbapServerProfile psp = mock(PbapServerProfile.class);
290         when(psp.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_pbap);
291         when(psp.toString()).thenReturn(PbapServerProfile.NAME);
292         when(psp.isProfileReady()).thenReturn(true);
293         when(mProfileManager.getPbapProfile()).thenReturn(psp);
294 
295         showScreen(mController);
296         List<SwitchPreference> switches = getProfileSwitches(false);
297         assertThat(switches.size()).isEqualTo(1);
298         SwitchPreference pref = switches.get(0);
299         assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_pbap));
300         assertThat(pref.isChecked()).isTrue();
301 
302         pref.performClick();
303         assertThat(mProfiles.getPreferenceCount()).isEqualTo(2);
304         assertThat(mDevice.getPhonebookAccessPermission())
305                 .isEqualTo(BluetoothDevice.ACCESS_REJECTED);
306     }
307 
308     @Test
pbapProfileStartsDisabled()309     public void pbapProfileStartsDisabled() {
310         setupDevice(makeDefaultDeviceConfig());
311         mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
312         PbapServerProfile psp = mock(PbapServerProfile.class);
313         when(psp.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_pbap);
314         when(psp.toString()).thenReturn(PbapServerProfile.NAME);
315         when(psp.isProfileReady()).thenReturn(true);
316         when(mProfileManager.getPbapProfile()).thenReturn(psp);
317 
318         showScreen(mController);
319         List<SwitchPreference> switches = getProfileSwitches(false);
320         assertThat(switches.size()).isEqualTo(1);
321         SwitchPreference pref = switches.get(0);
322         assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_pbap));
323         assertThat(pref.isChecked()).isFalse();
324 
325         pref.performClick();
326         assertThat(mProfiles.getPreferenceCount()).isEqualTo(2);
327         assertThat(mDevice.getPhonebookAccessPermission())
328                 .isEqualTo(BluetoothDevice.ACCESS_ALLOWED);
329     }
330 
331     @Test
mapProfile()332     public void mapProfile() {
333         setupDevice(makeDefaultDeviceConfig());
334         MapProfile mapProfile = mock(MapProfile.class);
335         when(mapProfile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_map);
336         when(mapProfile.isProfileReady()).thenReturn(true);
337         when(mProfileManager.getMapProfile()).thenReturn(mapProfile);
338         when(mProfileManager.getProfileByName(eq(mapProfile.toString()))).thenReturn(mapProfile);
339         mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
340         showScreen(mController);
341         List<SwitchPreference> switches = getProfileSwitches(false);
342         assertThat(switches.size()).isEqualTo(1);
343         SwitchPreference pref = switches.get(0);
344         assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_map));
345         assertThat(pref.isChecked()).isFalse();
346 
347         pref.performClick();
348         assertThat(mProfiles.getPreferenceCount()).isEqualTo(2);
349         assertThat(mDevice.getMessageAccessPermission()).isEqualTo(BluetoothDevice.ACCESS_ALLOWED);
350     }
351 
addMockA2dpProfile(boolean preferred, boolean supportsHighQualityAudio, boolean highQualityAudioEnabled)352     private A2dpProfile addMockA2dpProfile(boolean preferred, boolean supportsHighQualityAudio,
353             boolean highQualityAudioEnabled) {
354         A2dpProfile profile = mock(A2dpProfile.class);
355         when(mProfileManager.getProfileByName(eq(profile.toString()))).thenReturn(profile);
356         when(profile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_a2dp);
357         when(profile.getHighQualityAudioOptionLabel(mDevice)).thenReturn(
358             mContext.getString(R.string.bluetooth_profile_a2dp_high_quality_unknown_codec));
359         when(profile.supportsHighQualityAudio(mDevice)).thenReturn(supportsHighQualityAudio);
360         when(profile.isHighQualityAudioEnabled(mDevice)).thenReturn(highQualityAudioEnabled);
361         when(profile.isEnabled(mDevice)).thenReturn(preferred);
362         when(profile.isProfileReady()).thenReturn(true);
363         mConnectableProfiles.add(profile);
364         return profile;
365     }
366 
getHighQualityAudioPref()367     private SwitchPreference getHighQualityAudioPref() {
368         return (SwitchPreference) mScreen.findPreference(
369                 BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG);
370     }
371 
372     @Test
highQualityAudio_prefIsPresentWhenSupported()373     public void highQualityAudio_prefIsPresentWhenSupported() {
374         setupDevice(makeDefaultDeviceConfig());
375         addMockA2dpProfile(true, true, true);
376         showScreen(mController);
377         SwitchPreference pref = getHighQualityAudioPref();
378         assertThat(pref.getKey()).isEqualTo(
379                 BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG);
380 
381         // Make sure the preference works when clicked on.
382         pref.performClick();
383         A2dpProfile profile = (A2dpProfile) mConnectableProfiles.get(0);
384         verify(profile).setHighQualityAudioEnabled(mDevice, false);
385         pref.performClick();
386         verify(profile).setHighQualityAudioEnabled(mDevice, true);
387     }
388 
389     @Test
highQualityAudio_prefIsAbsentWhenNotSupported()390     public void highQualityAudio_prefIsAbsentWhenNotSupported() {
391         setupDevice(makeDefaultDeviceConfig());
392         addMockA2dpProfile(true, false, false);
393         showScreen(mController);
394         assertThat(mProfiles.getPreferenceCount()).isEqualTo(2);
395         SwitchPreference pref = (SwitchPreference) mProfiles.getPreference(0);
396         assertThat(pref.getKey())
397             .isNotEqualTo(BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG);
398         assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_a2dp));
399     }
400 
401     @Test
highQualityAudio_busyDeviceDisablesSwitch()402     public void highQualityAudio_busyDeviceDisablesSwitch() {
403         setupDevice(makeDefaultDeviceConfig());
404         addMockA2dpProfile(true, true, true);
405         when(mCachedDevice.isBusy()).thenReturn(true);
406         showScreen(mController);
407         SwitchPreference pref = getHighQualityAudioPref();
408         assertThat(pref.isEnabled()).isFalse();
409     }
410 
411     @Test
highQualityAudio_mediaAudioDisabledAndReEnabled()412     public void highQualityAudio_mediaAudioDisabledAndReEnabled() {
413         setupDevice(makeDefaultDeviceConfig());
414         A2dpProfile audioProfile = addMockA2dpProfile(true, true, true);
415         showScreen(mController);
416         assertThat(mProfiles.getPreferenceCount()).isEqualTo(3);
417 
418         // Disabling media audio should cause the high quality audio switch to disappear, but not
419         // the regular audio one.
420         SwitchPreference audioPref =
421             (SwitchPreference) mScreen.findPreference(audioProfile.toString());
422         audioPref.performClick();
423         verify(audioProfile).setEnabled(mDevice, false);
424         when(audioProfile.isEnabled(mDevice)).thenReturn(false);
425         mController.onDeviceAttributesChanged();
426         assertThat(audioPref.isVisible()).isTrue();
427         SwitchPreference highQualityAudioPref = getHighQualityAudioPref();
428         assertThat(highQualityAudioPref.isVisible()).isFalse();
429 
430         // And re-enabling media audio should make high quality switch to reappear.
431         audioPref.performClick();
432         verify(audioProfile).setEnabled(mDevice, true);
433         when(audioProfile.isEnabled(mDevice)).thenReturn(true);
434         mController.onDeviceAttributesChanged();
435         highQualityAudioPref = getHighQualityAudioPref();
436         assertThat(highQualityAudioPref.isVisible()).isTrue();
437     }
438 
439     @Test
highQualityAudio_mediaAudioStartsDisabled()440     public void highQualityAudio_mediaAudioStartsDisabled() {
441         setupDevice(makeDefaultDeviceConfig());
442         A2dpProfile audioProfile = addMockA2dpProfile(false, true, true);
443         showScreen(mController);
444         SwitchPreference audioPref = mScreen.findPreference(audioProfile.toString());
445         SwitchPreference highQualityAudioPref = getHighQualityAudioPref();
446         assertThat(audioPref).isNotNull();
447         assertThat(audioPref.isChecked()).isFalse();
448         assertThat(highQualityAudioPref).isNotNull();
449         assertThat(highQualityAudioPref.isVisible()).isFalse();
450     }
451 
452     @Test
onResume_addServiceListener()453     public void onResume_addServiceListener() {
454         mController.onResume();
455 
456         verify(mProfileManager).addServiceListener(mController);
457     }
458 
459     @Test
onPause_removeServiceListener()460     public void onPause_removeServiceListener() {
461         mController.onPause();
462 
463         verify(mProfileManager).removeServiceListener(mController);
464     }
465 }
466