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 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 protected HashSet<BluetoothDevice> mConnectedDevices; 91 protected HashMap<BluetoothDevice, Boolean> mPreferred; 92 protected Context mContext; 93 protected int mNameResourceId; 94 FakeBluetoothProfile(Context context, int nameResourceId)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 toString()103 public String toString() { 104 return mContext.getString(mNameResourceId); 105 } 106 107 @Override isConnectable()108 public boolean isConnectable() { 109 return true; 110 } 111 112 @Override isAutoConnectable()113 public boolean isAutoConnectable() { 114 return true; 115 } 116 117 @Override connect(BluetoothDevice device)118 public boolean connect(BluetoothDevice device) { 119 mConnectedDevices.add(device); 120 return true; 121 } 122 123 @Override disconnect(BluetoothDevice device)124 public boolean disconnect(BluetoothDevice device) { 125 mConnectedDevices.remove(device); 126 return false; 127 } 128 129 @Override getConnectionStatus(BluetoothDevice device)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 isPreferred(BluetoothDevice device)139 public boolean isPreferred(BluetoothDevice device) { 140 return mPreferred.getOrDefault(device, false); 141 } 142 143 @Override getPreferred(BluetoothDevice device)144 public int getPreferred(BluetoothDevice device) { 145 return isPreferred(device) ? 146 BluetoothProfile.PRIORITY_ON : BluetoothProfile.PRIORITY_OFF; 147 } 148 149 @Override setPreferred(BluetoothDevice device, boolean preferred)150 public void setPreferred(BluetoothDevice device, boolean preferred) { 151 mPreferred.put(device, preferred); 152 } 153 154 @Override isProfileReady()155 public boolean isProfileReady() { 156 return true; 157 } 158 159 @Override getOrdinal()160 public int getOrdinal() { 161 return 0; 162 } 163 164 @Override getNameResource(BluetoothDevice device)165 public int getNameResource(BluetoothDevice device) { 166 return mNameResourceId; 167 } 168 169 @Override getSummaryResourceForDevice(BluetoothDevice device)170 public int getSummaryResourceForDevice(BluetoothDevice device) { 171 return Utils.getConnectionStateSummary(getConnectionStatus(device)); 172 } 173 174 @Override getDrawableResource(BluetoothClass btClass)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 */ addFakeProfile(int profileNameResId, boolean deviceIsPreferred)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 */ getProfileSwitches(boolean expectOnlyMConnectable)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 verifyProfileSwitchTitles(List<SwitchPreference> switches)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 oneProfile()219 public void oneProfile() { 220 addFakeProfile(R.string.bluetooth_profile_a2dp, true); 221 showScreen(mController); 222 verifyProfileSwitchTitles(getProfileSwitches(true)); 223 } 224 225 @Test multipleProfiles()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 disableThenReenableOneProfile()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 disconnectedDeviceOneProfile()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 pbapProfileStartsEnabled()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 pbapProfileStartsDisabled()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 mapProfile()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 addMockA2dpProfile(boolean preferred, boolean supportsHighQualityAudio, boolean highQualityAudioEnabled)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 getHighQualityAudioPref()364 private SwitchPreference getHighQualityAudioPref() { 365 return (SwitchPreference) mScreen.findPreference( 366 BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); 367 } 368 369 @Test highQualityAudio_prefIsPresentWhenSupported()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 highQualityAudio_prefIsAbsentWhenNotSupported()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 highQualityAudio_busyDeviceDisablesSwitch()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 highQualityAudio_mediaAudioDisabledAndReEnabled()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 highQualityAudio_mediaAudioStartsDisabled()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