Home | History | Annotate | Download | only in bluetooth
      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.Matchers.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 import android.support.v14.preference.SwitchPreference;
     32 import android.support.v7.preference.PreferenceCategory;
     33 
     34 import com.android.settings.R;
     35 import com.android.settings.testutils.SettingsRobolectricTestRunner;
     36 import com.android.settings.TestConfig;
     37 import com.android.settings.testutils.shadow.SettingsShadowBluetoothDevice;
     38 import com.android.settingslib.bluetooth.A2dpProfile;
     39 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
     40 import com.android.settingslib.bluetooth.LocalBluetoothManager;
     41 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
     42 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
     43 import com.android.settingslib.bluetooth.MapProfile;
     44 import com.android.settingslib.bluetooth.PbapServerProfile;
     45 
     46 import org.junit.Test;
     47 import org.junit.runner.RunWith;
     48 import org.mockito.Mock;
     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 
     56 @RunWith(SettingsRobolectricTestRunner.class)
     57 @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
     58         shadows=SettingsShadowBluetoothDevice.class)
     59 public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsControllerTestBase {
     60     private BluetoothDetailsProfilesController mController;
     61     private List<LocalBluetoothProfile> mConnectableProfiles;
     62     private PreferenceCategory mProfiles;
     63 
     64     @Mock
     65     private LocalBluetoothManager mLocalManager;
     66     @Mock
     67     private LocalBluetoothProfileManager mProfileManager;
     68 
     69     @Override
     70     public void setUp() {
     71         super.setUp();
     72 
     73         mProfiles = spy(new PreferenceCategory(mContext));
     74         when(mProfiles.getPreferenceManager()).thenReturn(mPreferenceManager);
     75 
     76         mConnectableProfiles = new ArrayList<>();
     77         when(mLocalManager.getProfileManager()).thenReturn(mProfileManager);
     78         when(mCachedDevice.getConnectableProfiles()).thenAnswer(invocation ->
     79             new ArrayList<>(mConnectableProfiles)
     80         );
     81 
     82         setupDevice(mDeviceConfig);
     83         mController = new BluetoothDetailsProfilesController(mContext, mFragment, mLocalManager,
     84                 mCachedDevice, mLifecycle);
     85         mProfiles.setKey(mController.getPreferenceKey());
     86         mScreen.addPreference(mProfiles);
     87     }
     88 
     89     static class FakeBluetoothProfile implements LocalBluetoothProfile {
     90         protected HashSet<BluetoothDevice> mConnectedDevices;
     91         protected HashMap<BluetoothDevice, Boolean> mPreferred;
     92         protected Context mContext;
     93         protected int mNameResourceId;
     94 
     95         public FakeBluetoothProfile(Context context, int nameResourceId) {
     96             mConnectedDevices = new HashSet<>();
     97             mPreferred = new HashMap<>();
     98             mContext = context;
     99             mNameResourceId = nameResourceId;
    100         }
    101 
    102         @Override
    103         public String toString() {
    104             return mContext.getString(mNameResourceId);
    105         }
    106 
    107         @Override
    108         public boolean isConnectable() {
    109             return true;
    110         }
    111 
    112         @Override
    113         public boolean isAutoConnectable() {
    114             return true;
    115         }
    116 
    117         @Override
    118         public boolean connect(BluetoothDevice device) {
    119             mConnectedDevices.add(device);
    120             return true;
    121         }
    122 
    123         @Override
    124         public boolean disconnect(BluetoothDevice device) {
    125             mConnectedDevices.remove(device);
    126             return false;
    127         }
    128 
    129         @Override
    130         public int getConnectionStatus(BluetoothDevice device) {
    131             if (mConnectedDevices.contains(device)) {
    132                 return BluetoothProfile.STATE_CONNECTED;
    133             } else {
    134                 return BluetoothProfile.STATE_DISCONNECTED;
    135             }
    136         }
    137 
    138         @Override
    139         public boolean isPreferred(BluetoothDevice device) {
    140             return mPreferred.getOrDefault(device, false);
    141         }
    142 
    143         @Override
    144         public int getPreferred(BluetoothDevice device) {
    145             return isPreferred(device) ?
    146                     BluetoothProfile.PRIORITY_ON : BluetoothProfile.PRIORITY_OFF;
    147         }
    148 
    149         @Override
    150         public void setPreferred(BluetoothDevice device, boolean preferred) {
    151             mPreferred.put(device, preferred);
    152         }
    153 
    154         @Override
    155         public boolean isProfileReady() {
    156             return true;
    157         }
    158 
    159         @Override
    160         public int getOrdinal() {
    161             return 0;
    162         }
    163 
    164         @Override
    165         public int getNameResource(BluetoothDevice device) {
    166             return mNameResourceId;
    167         }
    168 
    169         @Override
    170         public int getSummaryResourceForDevice(BluetoothDevice device) {
    171             return Utils.getConnectionStateSummary(getConnectionStatus(device));
    172         }
    173 
    174         @Override
    175         public int getDrawableResource(BluetoothClass btClass) {
    176             return 0;
    177         }
    178     }
    179 
    180     /**
    181      * Creates and adds a mock LocalBluetoothProfile to the list of connectable profiles for the
    182      * device.
    183      @param profileNameResId  the resource id for the name used by this profile
    184      @param deviceIsPreferred  whether this profile should start out as enabled for the device
    185      */
    186     private LocalBluetoothProfile addFakeProfile(int profileNameResId,
    187             boolean deviceIsPreferred) {
    188         LocalBluetoothProfile profile = new FakeBluetoothProfile(mContext, profileNameResId);
    189         profile.setPreferred(mDevice, deviceIsPreferred);
    190         mConnectableProfiles.add(profile);
    191         when(mProfileManager.getProfileByName(eq(profile.toString()))).thenReturn(profile);
    192         return profile;
    193     }
    194 
    195     /** Returns the list of SwitchPreference objects added to the screen - there should be one per
    196      *  Bluetooth profile.
    197      */
    198     private List<SwitchPreference> getProfileSwitches(boolean expectOnlyMConnectable) {
    199         if (expectOnlyMConnectable) {
    200             assertThat(mConnectableProfiles).isNotEmpty();
    201             assertThat(mProfiles.getPreferenceCount()).isEqualTo(mConnectableProfiles.size());
    202         }
    203         ArrayList<SwitchPreference> result = new ArrayList<>();
    204         for (int i = 0; i < mProfiles.getPreferenceCount(); i++) {
    205             result.add((SwitchPreference)mProfiles.getPreference(i));
    206         }
    207         return result;
    208     }
    209 
    210      private void verifyProfileSwitchTitles(List<SwitchPreference> switches) {
    211         for (int i = 0; i < switches.size(); i++) {
    212             String expectedTitle = mContext.getString(
    213                     mConnectableProfiles.get(i).getNameResource(mDevice));
    214             assertThat(switches.get(i).getTitle()).isEqualTo(expectedTitle);
    215         }
    216     }
    217 
    218     @Test
    219     public void oneProfile() {
    220         addFakeProfile(R.string.bluetooth_profile_a2dp, true);
    221         showScreen(mController);
    222         verifyProfileSwitchTitles(getProfileSwitches(true));
    223     }
    224 
    225     @Test
    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(2);
    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
    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).isPreferred(mDevice)).isFalse();
    264 
    265         // Make sure no new preferences were added.
    266         assertThat(mProfiles.getPreferenceCount()).isEqualTo(2);
    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).isPreferred(mDevice)).isTrue();
    272 
    273         // Make sure we still haven't gotten any new preferences added.
    274         assertThat(mProfiles.getPreferenceCount()).isEqualTo(2);
    275     }
    276 
    277     @Test
    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
    286     public void pbapProfileStartsEnabled() {
    287         setupDevice(makeDefaultDeviceConfig());
    288         when(mCachedDevice.getPhonebookPermissionChoice()).thenReturn(
    289                 CachedBluetoothDevice.ACCESS_ALLOWED);
    290         PbapServerProfile psp = mock(PbapServerProfile.class);
    291         when(psp.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_pbap);
    292         when(psp.toString()).thenReturn(PbapServerProfile.NAME);
    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(1);
    304         verify(mCachedDevice).setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
    305     }
    306 
    307     @Test
    308     public void pbapProfileStartsDisabled() {
    309         setupDevice(makeDefaultDeviceConfig());
    310         when(mCachedDevice.getPhonebookPermissionChoice()).thenReturn(
    311                 CachedBluetoothDevice.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(mProfileManager.getPbapProfile()).thenReturn(psp);
    316 
    317         showScreen(mController);
    318         List<SwitchPreference> switches = getProfileSwitches(false);
    319         assertThat(switches.size()).isEqualTo(1);
    320         SwitchPreference pref = switches.get(0);
    321         assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_pbap));
    322         assertThat(pref.isChecked()).isFalse();
    323 
    324         pref.performClick();
    325         assertThat(mProfiles.getPreferenceCount()).isEqualTo(1);
    326         verify(mCachedDevice).setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
    327     }
    328 
    329     @Test
    330     public void mapProfile() {
    331         setupDevice(makeDefaultDeviceConfig());
    332         MapProfile mapProfile = mock(MapProfile.class);
    333         when(mapProfile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_map);
    334         when(mProfileManager.getMapProfile()).thenReturn(mapProfile);
    335         when(mProfileManager.getProfileByName(eq(mapProfile.toString()))).thenReturn(mapProfile);
    336         when(mCachedDevice.getMessagePermissionChoice()).thenReturn(
    337                 CachedBluetoothDevice.ACCESS_REJECTED);
    338         showScreen(mController);
    339         List<SwitchPreference> switches = getProfileSwitches(false);
    340         assertThat(switches.size()).isEqualTo(1);
    341         SwitchPreference pref = switches.get(0);
    342         assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_map));
    343         assertThat(pref.isChecked()).isFalse();
    344 
    345         pref.performClick();
    346         assertThat(mProfiles.getPreferenceCount()).isEqualTo(1);
    347         verify(mCachedDevice).setMessagePermissionChoice(BluetoothDevice.ACCESS_ALLOWED);
    348     }
    349 
    350     private A2dpProfile addMockA2dpProfile(boolean preferred, boolean supportsHighQualityAudio,
    351             boolean highQualityAudioEnabled) {
    352         A2dpProfile profile = mock(A2dpProfile.class);
    353         when(mProfileManager.getProfileByName(eq(profile.toString()))).thenReturn(profile);
    354         when(profile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_a2dp);
    355         when(profile.getHighQualityAudioOptionLabel(mDevice)).thenReturn(mContext.getString(
    356                 R.string.bluetooth_profile_a2dp_high_quality_unknown_codec));
    357         when(profile.supportsHighQualityAudio(mDevice)).thenReturn(supportsHighQualityAudio);
    358         when(profile.isHighQualityAudioEnabled(mDevice)).thenReturn(highQualityAudioEnabled);
    359         when(profile.isPreferred(mDevice)).thenReturn(preferred);
    360         mConnectableProfiles.add(profile);
    361         return profile;
    362     }
    363 
    364     private SwitchPreference getHighQualityAudioPref() {
    365         return (SwitchPreference) mScreen.findPreference(
    366                 BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG);
    367     }
    368 
    369     @Test
    370     public void highQualityAudio_prefIsPresentWhenSupported() {
    371         setupDevice(makeDefaultDeviceConfig());
    372         addMockA2dpProfile(true, true, true);
    373         showScreen(mController);
    374         SwitchPreference pref = getHighQualityAudioPref();
    375         assertThat(pref.getKey()).isEqualTo(
    376                 BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG);
    377 
    378         // Make sure the preference works when clicked on.
    379         pref.performClick();
    380         A2dpProfile profile = (A2dpProfile) mConnectableProfiles.get(0);
    381         verify(profile).setHighQualityAudioEnabled(mDevice, false);
    382         pref.performClick();
    383         verify(profile).setHighQualityAudioEnabled(mDevice, true);
    384     }
    385 
    386     @Test
    387     public void highQualityAudio_prefIsAbsentWhenNotSupported() {
    388         setupDevice(makeDefaultDeviceConfig());
    389         addMockA2dpProfile(true, false, false);
    390         showScreen(mController);
    391         assertThat(mProfiles.getPreferenceCount()).isEqualTo(1);
    392         SwitchPreference pref = (SwitchPreference) mProfiles.getPreference(0);
    393         assertThat(pref.getKey()).isNotEqualTo(
    394                 BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG);
    395         assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_a2dp));
    396     }
    397 
    398     @Test
    399     public void highQualityAudio_busyDeviceDisablesSwitch() {
    400         setupDevice(makeDefaultDeviceConfig());
    401         addMockA2dpProfile(true, true, true);
    402         when(mCachedDevice.isBusy()).thenReturn(true);
    403         showScreen(mController);
    404         SwitchPreference pref = getHighQualityAudioPref();
    405         assertThat(pref.isEnabled()).isFalse();
    406     }
    407 
    408     @Test
    409     public void highQualityAudio_mediaAudioDisabledAndReEnabled() {
    410         setupDevice(makeDefaultDeviceConfig());
    411         A2dpProfile audioProfile = addMockA2dpProfile(true, true, true);
    412         showScreen(mController);
    413         assertThat(mProfiles.getPreferenceCount()).isEqualTo(2);
    414 
    415         // Disabling media audio should cause the high quality audio switch to disappear, but not
    416         // the regular audio one.
    417         SwitchPreference audioPref = (SwitchPreference) mScreen.findPreference(
    418                 audioProfile.toString());
    419         audioPref.performClick();
    420         verify(audioProfile).setPreferred(mDevice, false);
    421         when(audioProfile.isPreferred(mDevice)).thenReturn(false);
    422         mController.onDeviceAttributesChanged();
    423         assertThat(audioPref.isVisible()).isTrue();
    424         SwitchPreference highQualityAudioPref = getHighQualityAudioPref();
    425         assertThat(highQualityAudioPref.isVisible()).isFalse();
    426 
    427         // And re-enabling media audio should make high quality switch to reappear.
    428         audioPref.performClick();
    429         verify(audioProfile).setPreferred(mDevice, true);
    430         when(audioProfile.isPreferred(mDevice)).thenReturn(true);
    431         mController.onDeviceAttributesChanged();
    432         highQualityAudioPref = getHighQualityAudioPref();
    433         assertThat(highQualityAudioPref.isVisible()).isTrue();
    434     }
    435 
    436     @Test
    437     public void highQualityAudio_mediaAudioStartsDisabled() {
    438         setupDevice(makeDefaultDeviceConfig());
    439         A2dpProfile audioProfile = addMockA2dpProfile(false, true, true);
    440         showScreen(mController);
    441         SwitchPreference audioPref = (SwitchPreference) mScreen.findPreference(
    442                 audioProfile.toString());
    443         SwitchPreference highQualityAudioPref = getHighQualityAudioPref();
    444         assertThat(audioPref).isNotNull();
    445         assertThat(audioPref.isChecked()).isFalse();
    446         assertThat(highQualityAudioPref).isNotNull();
    447         assertThat(highQualityAudioPref.isVisible()).isFalse();
    448     }
    449 }
    450