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