blob: 57fd86b25d586c53dc372382aa0b1b6e9957f5c3 [file] [log] [blame]
/*
* 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<LocalBluetoothProfile> 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<BluetoothDevice> mConnectedDevices = new HashSet<>();
private Map<BluetoothDevice, Boolean> 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<SwitchPreference> getProfileSwitches(boolean expectOnlyMConnectable) {
if (expectOnlyMConnectable) {
assertThat(mConnectableProfiles).isNotEmpty();
assertThat(mProfiles.getPreferenceCount()).isEqualTo(mConnectableProfiles.size());
}
List<SwitchPreference> result = new ArrayList<>();
for (int i = 0; i < mProfiles.getPreferenceCount(); i++) {
result.add((SwitchPreference)mProfiles.getPreference(i));
}
return result;
}
private void verifyProfileSwitchTitles(List<SwitchPreference> 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<SwitchPreference> 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<SwitchPreference> 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<SwitchPreference> 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<SwitchPreference> 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<SwitchPreference> 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);
}
}