/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.bluetooth; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; import androidx.preference.PreferenceCategory; import androidx.preference.SwitchPreference; import com.android.settings.R; import com.android.settings.testutils.shadow.ShadowBluetoothDevice; import com.android.settingslib.bluetooth.A2dpProfile; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfile; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.bluetooth.MapProfile; import com.android.settingslib.bluetooth.PbapServerProfile; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @RunWith(RobolectricTestRunner.class) @Config(shadows = ShadowBluetoothDevice.class) public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsControllerTestBase { private BluetoothDetailsProfilesController mController; private List mConnectableProfiles; private PreferenceCategory mProfiles; @Mock private LocalBluetoothManager mLocalManager; @Mock private LocalBluetoothProfileManager mProfileManager; @Override public void setUp() { super.setUp(); mProfiles = spy(new PreferenceCategory(mContext)); when(mProfiles.getPreferenceManager()).thenReturn(mPreferenceManager); mConnectableProfiles = new ArrayList<>(); when(mLocalManager.getProfileManager()).thenReturn(mProfileManager); when(mCachedDevice.getConnectableProfiles()).thenAnswer(invocation -> new ArrayList<>(mConnectableProfiles) ); setupDevice(mDeviceConfig); mController = new BluetoothDetailsProfilesController(mContext, mFragment, mLocalManager, mCachedDevice, mLifecycle); mProfiles.setKey(mController.getPreferenceKey()); mScreen.addPreference(mProfiles); } static class FakeBluetoothProfile implements LocalBluetoothProfile { private Set mConnectedDevices = new HashSet<>(); private Map mPreferred = new HashMap<>(); private Context mContext; private int mNameResourceId; private FakeBluetoothProfile(Context context, int nameResourceId) { mContext = context; mNameResourceId = nameResourceId; } @Override public String toString() { return mContext.getString(mNameResourceId); } @Override public boolean accessProfileEnabled() { return true; } @Override public boolean isAutoConnectable() { return true; } @Override public boolean connect(BluetoothDevice device) { mConnectedDevices.add(device); return true; } @Override public boolean disconnect(BluetoothDevice device) { mConnectedDevices.remove(device); return false; } @Override public int getConnectionStatus(BluetoothDevice device) { if (mConnectedDevices.contains(device)) { return BluetoothProfile.STATE_CONNECTED; } else { return BluetoothProfile.STATE_DISCONNECTED; } } @Override public boolean isPreferred(BluetoothDevice device) { return mPreferred.getOrDefault(device, false); } @Override public int getPreferred(BluetoothDevice device) { return isPreferred(device) ? BluetoothProfile.PRIORITY_ON : BluetoothProfile.PRIORITY_OFF; } @Override public void setPreferred(BluetoothDevice device, boolean preferred) { mPreferred.put(device, preferred); } @Override public boolean isProfileReady() { return true; } @Override public int getProfileId() { return 0; } @Override public int getOrdinal() { return 0; } @Override public int getNameResource(BluetoothDevice device) { return mNameResourceId; } @Override public int getSummaryResourceForDevice(BluetoothDevice device) { return Utils.getConnectionStateSummary(getConnectionStatus(device)); } @Override public int getDrawableResource(BluetoothClass btClass) { return 0; } } /** * Creates and adds a mock LocalBluetoothProfile to the list of connectable profiles for the * device. * @param profileNameResId the resource id for the name used by this profile * @param deviceIsPreferred whether this profile should start out as enabled for the device */ private LocalBluetoothProfile addFakeProfile(int profileNameResId, boolean deviceIsPreferred) { LocalBluetoothProfile profile = new FakeBluetoothProfile(mContext, profileNameResId); profile.setPreferred(mDevice, deviceIsPreferred); mConnectableProfiles.add(profile); when(mProfileManager.getProfileByName(eq(profile.toString()))).thenReturn(profile); return profile; } /** Returns the list of SwitchPreference objects added to the screen - there should be one per * Bluetooth profile. */ private List getProfileSwitches(boolean expectOnlyMConnectable) { if (expectOnlyMConnectable) { assertThat(mConnectableProfiles).isNotEmpty(); assertThat(mProfiles.getPreferenceCount()).isEqualTo(mConnectableProfiles.size()); } List result = new ArrayList<>(); for (int i = 0; i < mProfiles.getPreferenceCount(); i++) { result.add((SwitchPreference)mProfiles.getPreference(i)); } return result; } private void verifyProfileSwitchTitles(List switches) { for (int i = 0; i < switches.size(); i++) { String expectedTitle = mContext.getString(mConnectableProfiles.get(i).getNameResource(mDevice)); assertThat(switches.get(i).getTitle()).isEqualTo(expectedTitle); } } @Test public void oneProfile() { addFakeProfile(R.string.bluetooth_profile_a2dp, true); showScreen(mController); verifyProfileSwitchTitles(getProfileSwitches(true)); } @Test public void multipleProfiles() { addFakeProfile(R.string.bluetooth_profile_a2dp, true); addFakeProfile(R.string.bluetooth_profile_headset, false); showScreen(mController); List switches = getProfileSwitches(true); verifyProfileSwitchTitles(switches); assertThat(switches.get(0).isChecked()).isTrue(); assertThat(switches.get(1).isChecked()).isFalse(); // Both switches should be enabled. assertThat(switches.get(0).isEnabled()).isTrue(); assertThat(switches.get(1).isEnabled()).isTrue(); // Make device busy. when(mCachedDevice.isBusy()).thenReturn(true); mController.onDeviceAttributesChanged(); // There should have been no new switches added. assertThat(mProfiles.getPreferenceCount()).isEqualTo(2); // Make sure both switches got disabled. assertThat(switches.get(0).isEnabled()).isFalse(); assertThat(switches.get(1).isEnabled()).isFalse(); } @Test public void disableThenReenableOneProfile() { addFakeProfile(R.string.bluetooth_profile_a2dp, true); addFakeProfile(R.string.bluetooth_profile_headset, true); showScreen(mController); List switches = getProfileSwitches(true); SwitchPreference pref = switches.get(0); // Clicking the pref should cause the profile to become not-preferred. assertThat(pref.isChecked()).isTrue(); pref.performClick(); assertThat(pref.isChecked()).isFalse(); assertThat(mConnectableProfiles.get(0).isPreferred(mDevice)).isFalse(); // Make sure no new preferences were added. assertThat(mProfiles.getPreferenceCount()).isEqualTo(2); // Clicking the pref again should make the profile once again preferred. pref.performClick(); assertThat(pref.isChecked()).isTrue(); assertThat(mConnectableProfiles.get(0).isPreferred(mDevice)).isTrue(); // Make sure we still haven't gotten any new preferences added. assertThat(mProfiles.getPreferenceCount()).isEqualTo(2); } @Test public void disconnectedDeviceOneProfile() { setupDevice(makeDefaultDeviceConfig().setConnected(false).setConnectionSummary(null)); addFakeProfile(R.string.bluetooth_profile_a2dp, true); showScreen(mController); verifyProfileSwitchTitles(getProfileSwitches(true)); } @Test public void pbapProfileStartsEnabled() { setupDevice(makeDefaultDeviceConfig()); mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); PbapServerProfile psp = mock(PbapServerProfile.class); when(psp.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_pbap); when(psp.toString()).thenReturn(PbapServerProfile.NAME); when(psp.isProfileReady()).thenReturn(true); when(mProfileManager.getPbapProfile()).thenReturn(psp); showScreen(mController); List switches = getProfileSwitches(false); assertThat(switches.size()).isEqualTo(1); SwitchPreference pref = switches.get(0); assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_pbap)); assertThat(pref.isChecked()).isTrue(); pref.performClick(); assertThat(mProfiles.getPreferenceCount()).isEqualTo(1); assertThat(mDevice.getPhonebookAccessPermission()) .isEqualTo(BluetoothDevice.ACCESS_REJECTED); } @Test public void pbapProfileStartsDisabled() { setupDevice(makeDefaultDeviceConfig()); mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); PbapServerProfile psp = mock(PbapServerProfile.class); when(psp.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_pbap); when(psp.toString()).thenReturn(PbapServerProfile.NAME); when(psp.isProfileReady()).thenReturn(true); when(mProfileManager.getPbapProfile()).thenReturn(psp); showScreen(mController); List switches = getProfileSwitches(false); assertThat(switches.size()).isEqualTo(1); SwitchPreference pref = switches.get(0); assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_pbap)); assertThat(pref.isChecked()).isFalse(); pref.performClick(); assertThat(mProfiles.getPreferenceCount()).isEqualTo(1); assertThat(mDevice.getPhonebookAccessPermission()) .isEqualTo(BluetoothDevice.ACCESS_ALLOWED); } @Test public void mapProfile() { setupDevice(makeDefaultDeviceConfig()); MapProfile mapProfile = mock(MapProfile.class); when(mapProfile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_map); when(mapProfile.isProfileReady()).thenReturn(true); when(mProfileManager.getMapProfile()).thenReturn(mapProfile); when(mProfileManager.getProfileByName(eq(mapProfile.toString()))).thenReturn(mapProfile); mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); showScreen(mController); List switches = getProfileSwitches(false); assertThat(switches.size()).isEqualTo(1); SwitchPreference pref = switches.get(0); assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_map)); assertThat(pref.isChecked()).isFalse(); pref.performClick(); assertThat(mProfiles.getPreferenceCount()).isEqualTo(1); assertThat(mDevice.getMessageAccessPermission()).isEqualTo(BluetoothDevice.ACCESS_ALLOWED); } private A2dpProfile addMockA2dpProfile(boolean preferred, boolean supportsHighQualityAudio, boolean highQualityAudioEnabled) { A2dpProfile profile = mock(A2dpProfile.class); when(mProfileManager.getProfileByName(eq(profile.toString()))).thenReturn(profile); when(profile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_a2dp); when(profile.getHighQualityAudioOptionLabel(mDevice)).thenReturn( mContext.getString(R.string.bluetooth_profile_a2dp_high_quality_unknown_codec)); when(profile.supportsHighQualityAudio(mDevice)).thenReturn(supportsHighQualityAudio); when(profile.isHighQualityAudioEnabled(mDevice)).thenReturn(highQualityAudioEnabled); when(profile.isPreferred(mDevice)).thenReturn(preferred); when(profile.isProfileReady()).thenReturn(true); mConnectableProfiles.add(profile); return profile; } private SwitchPreference getHighQualityAudioPref() { return (SwitchPreference) mScreen.findPreference( BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); } @Test public void highQualityAudio_prefIsPresentWhenSupported() { setupDevice(makeDefaultDeviceConfig()); addMockA2dpProfile(true, true, true); showScreen(mController); SwitchPreference pref = getHighQualityAudioPref(); assertThat(pref.getKey()).isEqualTo( BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); // Make sure the preference works when clicked on. pref.performClick(); A2dpProfile profile = (A2dpProfile) mConnectableProfiles.get(0); verify(profile).setHighQualityAudioEnabled(mDevice, false); pref.performClick(); verify(profile).setHighQualityAudioEnabled(mDevice, true); } @Test public void highQualityAudio_prefIsAbsentWhenNotSupported() { setupDevice(makeDefaultDeviceConfig()); addMockA2dpProfile(true, false, false); showScreen(mController); assertThat(mProfiles.getPreferenceCount()).isEqualTo(1); SwitchPreference pref = (SwitchPreference) mProfiles.getPreference(0); assertThat(pref.getKey()) .isNotEqualTo(BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_a2dp)); } @Test public void highQualityAudio_busyDeviceDisablesSwitch() { setupDevice(makeDefaultDeviceConfig()); addMockA2dpProfile(true, true, true); when(mCachedDevice.isBusy()).thenReturn(true); showScreen(mController); SwitchPreference pref = getHighQualityAudioPref(); assertThat(pref.isEnabled()).isFalse(); } @Test public void highQualityAudio_mediaAudioDisabledAndReEnabled() { setupDevice(makeDefaultDeviceConfig()); A2dpProfile audioProfile = addMockA2dpProfile(true, true, true); showScreen(mController); assertThat(mProfiles.getPreferenceCount()).isEqualTo(2); // Disabling media audio should cause the high quality audio switch to disappear, but not // the regular audio one. SwitchPreference audioPref = (SwitchPreference) mScreen.findPreference(audioProfile.toString()); audioPref.performClick(); verify(audioProfile).setPreferred(mDevice, false); when(audioProfile.isPreferred(mDevice)).thenReturn(false); mController.onDeviceAttributesChanged(); assertThat(audioPref.isVisible()).isTrue(); SwitchPreference highQualityAudioPref = getHighQualityAudioPref(); assertThat(highQualityAudioPref.isVisible()).isFalse(); // And re-enabling media audio should make high quality switch to reappear. audioPref.performClick(); verify(audioProfile).setPreferred(mDevice, true); when(audioProfile.isPreferred(mDevice)).thenReturn(true); mController.onDeviceAttributesChanged(); highQualityAudioPref = getHighQualityAudioPref(); assertThat(highQualityAudioPref.isVisible()).isTrue(); } @Test public void highQualityAudio_mediaAudioStartsDisabled() { setupDevice(makeDefaultDeviceConfig()); A2dpProfile audioProfile = addMockA2dpProfile(false, true, true); showScreen(mController); SwitchPreference audioPref = mScreen.findPreference(audioProfile.toString()); SwitchPreference highQualityAudioPref = getHighQualityAudioPref(); assertThat(audioPref).isNotNull(); assertThat(audioPref.isChecked()).isFalse(); assertThat(highQualityAudioPref).isNotNull(); assertThat(highQualityAudioPref.isVisible()).isFalse(); } @Test public void onResume_addServiceListener() { mController.onResume(); verify(mProfileManager).addServiceListener(mController); } @Test public void onPause_removeServiceListener() { mController.onPause(); verify(mProfileManager).removeServiceListener(mController); } }