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