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.ArgumentMatchers.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 32 import androidx.preference.PreferenceCategory; 33 import androidx.preference.SwitchPreference; 34 35 import com.android.settings.R; 36 import com.android.settings.testutils.shadow.ShadowBluetoothDevice; 37 import com.android.settingslib.bluetooth.A2dpProfile; 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.RobolectricTestRunner; 48 import org.robolectric.annotation.Config; 49 50 import java.util.ArrayList; 51 import java.util.HashMap; 52 import java.util.HashSet; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.Set; 56 57 @RunWith(RobolectricTestRunner.class) 58 @Config(shadows = ShadowBluetoothDevice.class) 59 public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsControllerTestBase { 60 61 private BluetoothDetailsProfilesController mController; 62 private List<LocalBluetoothProfile> mConnectableProfiles; 63 private PreferenceCategory mProfiles; 64 65 @Mock 66 private LocalBluetoothManager mLocalManager; 67 @Mock 68 private LocalBluetoothProfileManager mProfileManager; 69 70 @Override setUp()71 public void setUp() { 72 super.setUp(); 73 74 mProfiles = spy(new PreferenceCategory(mContext)); 75 when(mProfiles.getPreferenceManager()).thenReturn(mPreferenceManager); 76 77 mConnectableProfiles = new ArrayList<>(); 78 when(mLocalManager.getProfileManager()).thenReturn(mProfileManager); 79 when(mCachedDevice.getConnectableProfiles()).thenAnswer(invocation -> 80 new ArrayList<>(mConnectableProfiles) 81 ); 82 83 setupDevice(mDeviceConfig); 84 mController = new BluetoothDetailsProfilesController(mContext, mFragment, mLocalManager, 85 mCachedDevice, mLifecycle); 86 mProfiles.setKey(mController.getPreferenceKey()); 87 mScreen.addPreference(mProfiles); 88 } 89 90 static class FakeBluetoothProfile implements LocalBluetoothProfile { 91 92 private Set<BluetoothDevice> mConnectedDevices = new HashSet<>(); 93 private Map<BluetoothDevice, Boolean> mPreferred = new HashMap<>(); 94 private Context mContext; 95 private int mNameResourceId; 96 FakeBluetoothProfile(Context context, int nameResourceId)97 private FakeBluetoothProfile(Context context, int nameResourceId) { 98 mContext = context; 99 mNameResourceId = nameResourceId; 100 } 101 102 @Override toString()103 public String toString() { 104 return mContext.getString(mNameResourceId); 105 } 106 107 @Override accessProfileEnabled()108 public boolean accessProfileEnabled() { 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 getProfileId()160 public int getProfileId() { 161 return 0; 162 } 163 164 @Override getOrdinal()165 public int getOrdinal() { 166 return 0; 167 } 168 169 @Override getNameResource(BluetoothDevice device)170 public int getNameResource(BluetoothDevice device) { 171 return mNameResourceId; 172 } 173 174 @Override getSummaryResourceForDevice(BluetoothDevice device)175 public int getSummaryResourceForDevice(BluetoothDevice device) { 176 return Utils.getConnectionStateSummary(getConnectionStatus(device)); 177 } 178 179 @Override getDrawableResource(BluetoothClass btClass)180 public int getDrawableResource(BluetoothClass btClass) { 181 return 0; 182 } 183 } 184 185 /** 186 * Creates and adds a mock LocalBluetoothProfile to the list of connectable profiles for the 187 * device. 188 * @param profileNameResId the resource id for the name used by this profile 189 * @param deviceIsPreferred whether this profile should start out as enabled for the device 190 */ addFakeProfile(int profileNameResId, boolean deviceIsPreferred)191 private LocalBluetoothProfile addFakeProfile(int profileNameResId, 192 boolean deviceIsPreferred) { 193 LocalBluetoothProfile profile = new FakeBluetoothProfile(mContext, profileNameResId); 194 profile.setPreferred(mDevice, deviceIsPreferred); 195 mConnectableProfiles.add(profile); 196 when(mProfileManager.getProfileByName(eq(profile.toString()))).thenReturn(profile); 197 return profile; 198 } 199 200 /** Returns the list of SwitchPreference objects added to the screen - there should be one per 201 * Bluetooth profile. 202 */ getProfileSwitches(boolean expectOnlyMConnectable)203 private List<SwitchPreference> getProfileSwitches(boolean expectOnlyMConnectable) { 204 if (expectOnlyMConnectable) { 205 assertThat(mConnectableProfiles).isNotEmpty(); 206 assertThat(mProfiles.getPreferenceCount()).isEqualTo(mConnectableProfiles.size()); 207 } 208 List<SwitchPreference> result = new ArrayList<>(); 209 for (int i = 0; i < mProfiles.getPreferenceCount(); i++) { 210 result.add((SwitchPreference)mProfiles.getPreference(i)); 211 } 212 return result; 213 } 214 verifyProfileSwitchTitles(List<SwitchPreference> switches)215 private void verifyProfileSwitchTitles(List<SwitchPreference> switches) { 216 for (int i = 0; i < switches.size(); i++) { 217 String expectedTitle = 218 mContext.getString(mConnectableProfiles.get(i).getNameResource(mDevice)); 219 assertThat(switches.get(i).getTitle()).isEqualTo(expectedTitle); 220 } 221 } 222 223 @Test oneProfile()224 public void oneProfile() { 225 addFakeProfile(R.string.bluetooth_profile_a2dp, true); 226 showScreen(mController); 227 verifyProfileSwitchTitles(getProfileSwitches(true)); 228 } 229 230 @Test multipleProfiles()231 public void multipleProfiles() { 232 addFakeProfile(R.string.bluetooth_profile_a2dp, true); 233 addFakeProfile(R.string.bluetooth_profile_headset, false); 234 showScreen(mController); 235 List<SwitchPreference> switches = getProfileSwitches(true); 236 verifyProfileSwitchTitles(switches); 237 assertThat(switches.get(0).isChecked()).isTrue(); 238 assertThat(switches.get(1).isChecked()).isFalse(); 239 240 // Both switches should be enabled. 241 assertThat(switches.get(0).isEnabled()).isTrue(); 242 assertThat(switches.get(1).isEnabled()).isTrue(); 243 244 // Make device busy. 245 when(mCachedDevice.isBusy()).thenReturn(true); 246 mController.onDeviceAttributesChanged(); 247 248 // There should have been no new switches added. 249 assertThat(mProfiles.getPreferenceCount()).isEqualTo(2); 250 251 // Make sure both switches got disabled. 252 assertThat(switches.get(0).isEnabled()).isFalse(); 253 assertThat(switches.get(1).isEnabled()).isFalse(); 254 } 255 256 @Test disableThenReenableOneProfile()257 public void disableThenReenableOneProfile() { 258 addFakeProfile(R.string.bluetooth_profile_a2dp, true); 259 addFakeProfile(R.string.bluetooth_profile_headset, true); 260 showScreen(mController); 261 List<SwitchPreference> switches = getProfileSwitches(true); 262 SwitchPreference pref = switches.get(0); 263 264 // Clicking the pref should cause the profile to become not-preferred. 265 assertThat(pref.isChecked()).isTrue(); 266 pref.performClick(); 267 assertThat(pref.isChecked()).isFalse(); 268 assertThat(mConnectableProfiles.get(0).isPreferred(mDevice)).isFalse(); 269 270 // Make sure no new preferences were added. 271 assertThat(mProfiles.getPreferenceCount()).isEqualTo(2); 272 273 // Clicking the pref again should make the profile once again preferred. 274 pref.performClick(); 275 assertThat(pref.isChecked()).isTrue(); 276 assertThat(mConnectableProfiles.get(0).isPreferred(mDevice)).isTrue(); 277 278 // Make sure we still haven't gotten any new preferences added. 279 assertThat(mProfiles.getPreferenceCount()).isEqualTo(2); 280 } 281 282 @Test disconnectedDeviceOneProfile()283 public void disconnectedDeviceOneProfile() { 284 setupDevice(makeDefaultDeviceConfig().setConnected(false).setConnectionSummary(null)); 285 addFakeProfile(R.string.bluetooth_profile_a2dp, true); 286 showScreen(mController); 287 verifyProfileSwitchTitles(getProfileSwitches(true)); 288 } 289 290 @Test pbapProfileStartsEnabled()291 public void pbapProfileStartsEnabled() { 292 setupDevice(makeDefaultDeviceConfig()); 293 mDevice.setPhonebookAccessPermission(BluetoothDevice.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(psp.isProfileReady()).thenReturn(true); 298 when(mProfileManager.getPbapProfile()).thenReturn(psp); 299 300 showScreen(mController); 301 List<SwitchPreference> switches = getProfileSwitches(false); 302 assertThat(switches.size()).isEqualTo(1); 303 SwitchPreference pref = switches.get(0); 304 assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_pbap)); 305 assertThat(pref.isChecked()).isTrue(); 306 307 pref.performClick(); 308 assertThat(mProfiles.getPreferenceCount()).isEqualTo(1); 309 assertThat(mDevice.getPhonebookAccessPermission()) 310 .isEqualTo(BluetoothDevice.ACCESS_REJECTED); 311 } 312 313 @Test pbapProfileStartsDisabled()314 public void pbapProfileStartsDisabled() { 315 setupDevice(makeDefaultDeviceConfig()); 316 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 317 PbapServerProfile psp = mock(PbapServerProfile.class); 318 when(psp.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_pbap); 319 when(psp.toString()).thenReturn(PbapServerProfile.NAME); 320 when(psp.isProfileReady()).thenReturn(true); 321 when(mProfileManager.getPbapProfile()).thenReturn(psp); 322 323 showScreen(mController); 324 List<SwitchPreference> switches = getProfileSwitches(false); 325 assertThat(switches.size()).isEqualTo(1); 326 SwitchPreference pref = switches.get(0); 327 assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_pbap)); 328 assertThat(pref.isChecked()).isFalse(); 329 330 pref.performClick(); 331 assertThat(mProfiles.getPreferenceCount()).isEqualTo(1); 332 assertThat(mDevice.getPhonebookAccessPermission()) 333 .isEqualTo(BluetoothDevice.ACCESS_ALLOWED); 334 } 335 336 @Test mapProfile()337 public void mapProfile() { 338 setupDevice(makeDefaultDeviceConfig()); 339 MapProfile mapProfile = mock(MapProfile.class); 340 when(mapProfile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_map); 341 when(mapProfile.isProfileReady()).thenReturn(true); 342 when(mProfileManager.getMapProfile()).thenReturn(mapProfile); 343 when(mProfileManager.getProfileByName(eq(mapProfile.toString()))).thenReturn(mapProfile); 344 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 345 showScreen(mController); 346 List<SwitchPreference> switches = getProfileSwitches(false); 347 assertThat(switches.size()).isEqualTo(1); 348 SwitchPreference pref = switches.get(0); 349 assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_map)); 350 assertThat(pref.isChecked()).isFalse(); 351 352 pref.performClick(); 353 assertThat(mProfiles.getPreferenceCount()).isEqualTo(1); 354 assertThat(mDevice.getMessageAccessPermission()).isEqualTo(BluetoothDevice.ACCESS_ALLOWED); 355 } 356 addMockA2dpProfile(boolean preferred, boolean supportsHighQualityAudio, boolean highQualityAudioEnabled)357 private A2dpProfile addMockA2dpProfile(boolean preferred, boolean supportsHighQualityAudio, 358 boolean highQualityAudioEnabled) { 359 A2dpProfile profile = mock(A2dpProfile.class); 360 when(mProfileManager.getProfileByName(eq(profile.toString()))).thenReturn(profile); 361 when(profile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_a2dp); 362 when(profile.getHighQualityAudioOptionLabel(mDevice)).thenReturn( 363 mContext.getString(R.string.bluetooth_profile_a2dp_high_quality_unknown_codec)); 364 when(profile.supportsHighQualityAudio(mDevice)).thenReturn(supportsHighQualityAudio); 365 when(profile.isHighQualityAudioEnabled(mDevice)).thenReturn(highQualityAudioEnabled); 366 when(profile.isPreferred(mDevice)).thenReturn(preferred); 367 when(profile.isProfileReady()).thenReturn(true); 368 mConnectableProfiles.add(profile); 369 return profile; 370 } 371 getHighQualityAudioPref()372 private SwitchPreference getHighQualityAudioPref() { 373 return (SwitchPreference) mScreen.findPreference( 374 BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); 375 } 376 377 @Test highQualityAudio_prefIsPresentWhenSupported()378 public void highQualityAudio_prefIsPresentWhenSupported() { 379 setupDevice(makeDefaultDeviceConfig()); 380 addMockA2dpProfile(true, true, true); 381 showScreen(mController); 382 SwitchPreference pref = getHighQualityAudioPref(); 383 assertThat(pref.getKey()).isEqualTo( 384 BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); 385 386 // Make sure the preference works when clicked on. 387 pref.performClick(); 388 A2dpProfile profile = (A2dpProfile) mConnectableProfiles.get(0); 389 verify(profile).setHighQualityAudioEnabled(mDevice, false); 390 pref.performClick(); 391 verify(profile).setHighQualityAudioEnabled(mDevice, true); 392 } 393 394 @Test highQualityAudio_prefIsAbsentWhenNotSupported()395 public void highQualityAudio_prefIsAbsentWhenNotSupported() { 396 setupDevice(makeDefaultDeviceConfig()); 397 addMockA2dpProfile(true, false, false); 398 showScreen(mController); 399 assertThat(mProfiles.getPreferenceCount()).isEqualTo(1); 400 SwitchPreference pref = (SwitchPreference) mProfiles.getPreference(0); 401 assertThat(pref.getKey()) 402 .isNotEqualTo(BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); 403 assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_a2dp)); 404 } 405 406 @Test highQualityAudio_busyDeviceDisablesSwitch()407 public void highQualityAudio_busyDeviceDisablesSwitch() { 408 setupDevice(makeDefaultDeviceConfig()); 409 addMockA2dpProfile(true, true, true); 410 when(mCachedDevice.isBusy()).thenReturn(true); 411 showScreen(mController); 412 SwitchPreference pref = getHighQualityAudioPref(); 413 assertThat(pref.isEnabled()).isFalse(); 414 } 415 416 @Test highQualityAudio_mediaAudioDisabledAndReEnabled()417 public void highQualityAudio_mediaAudioDisabledAndReEnabled() { 418 setupDevice(makeDefaultDeviceConfig()); 419 A2dpProfile audioProfile = addMockA2dpProfile(true, true, true); 420 showScreen(mController); 421 assertThat(mProfiles.getPreferenceCount()).isEqualTo(2); 422 423 // Disabling media audio should cause the high quality audio switch to disappear, but not 424 // the regular audio one. 425 SwitchPreference audioPref = 426 (SwitchPreference) mScreen.findPreference(audioProfile.toString()); 427 audioPref.performClick(); 428 verify(audioProfile).setPreferred(mDevice, false); 429 when(audioProfile.isPreferred(mDevice)).thenReturn(false); 430 mController.onDeviceAttributesChanged(); 431 assertThat(audioPref.isVisible()).isTrue(); 432 SwitchPreference highQualityAudioPref = getHighQualityAudioPref(); 433 assertThat(highQualityAudioPref.isVisible()).isFalse(); 434 435 // And re-enabling media audio should make high quality switch to reappear. 436 audioPref.performClick(); 437 verify(audioProfile).setPreferred(mDevice, true); 438 when(audioProfile.isPreferred(mDevice)).thenReturn(true); 439 mController.onDeviceAttributesChanged(); 440 highQualityAudioPref = getHighQualityAudioPref(); 441 assertThat(highQualityAudioPref.isVisible()).isTrue(); 442 } 443 444 @Test highQualityAudio_mediaAudioStartsDisabled()445 public void highQualityAudio_mediaAudioStartsDisabled() { 446 setupDevice(makeDefaultDeviceConfig()); 447 A2dpProfile audioProfile = addMockA2dpProfile(false, true, true); 448 showScreen(mController); 449 SwitchPreference audioPref = mScreen.findPreference(audioProfile.toString()); 450 SwitchPreference highQualityAudioPref = getHighQualityAudioPref(); 451 assertThat(audioPref).isNotNull(); 452 assertThat(audioPref.isChecked()).isFalse(); 453 assertThat(highQualityAudioPref).isNotNull(); 454 assertThat(highQualityAudioPref.isVisible()).isFalse(); 455 } 456 457 @Test onResume_addServiceListener()458 public void onResume_addServiceListener() { 459 mController.onResume(); 460 461 verify(mProfileManager).addServiceListener(mController); 462 } 463 464 @Test onPause_removeServiceListener()465 public void onPause_removeServiceListener() { 466 mController.onPause(); 467 468 verify(mProfileManager).removeServiceListener(mController); 469 } 470 } 471