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