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.Test; 46 import org.junit.runner.RunWith; 47 import org.mockito.Mock; 48 import org.robolectric.RobolectricTestRunner; 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 import java.util.Map; 56 import java.util.Set; 57 58 @RunWith(RobolectricTestRunner.class) 59 @Config(shadows = ShadowBluetoothDevice.class) 60 public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsControllerTestBase { 61 62 private BluetoothDetailsProfilesController mController; 63 private List<LocalBluetoothProfile> mConnectableProfiles; 64 private PreferenceCategory mProfiles; 65 66 @Mock 67 private LocalBluetoothManager mLocalManager; 68 @Mock 69 private LocalBluetoothProfileManager mProfileManager; 70 71 @Override setUp()72 public void setUp() { 73 super.setUp(); 74 75 mProfiles = spy(new PreferenceCategory(mContext)); 76 when(mProfiles.getPreferenceManager()).thenReturn(mPreferenceManager); 77 78 mConnectableProfiles = new ArrayList<>(); 79 when(mLocalManager.getProfileManager()).thenReturn(mProfileManager); 80 when(mCachedDevice.getConnectableProfiles()).thenAnswer(invocation -> 81 new ArrayList<>(mConnectableProfiles) 82 ); 83 84 setupDevice(mDeviceConfig); 85 mController = new BluetoothDetailsProfilesController(mContext, mFragment, mLocalManager, 86 mCachedDevice, mLifecycle); 87 mProfiles.setKey(mController.getPreferenceKey()); 88 mController.mProfilesContainer = mProfiles; 89 mScreen.addPreference(mProfiles); 90 } 91 92 static class FakeBluetoothProfile implements LocalBluetoothProfile { 93 94 private Set<BluetoothDevice> mConnectedDevices = new HashSet<>(); 95 private Map<BluetoothDevice, Boolean> mPreferred = new HashMap<>(); 96 private Context mContext; 97 private int mNameResourceId; 98 FakeBluetoothProfile(Context context, int nameResourceId)99 private FakeBluetoothProfile(Context context, int nameResourceId) { 100 mContext = context; 101 mNameResourceId = nameResourceId; 102 } 103 104 @Override toString()105 public String toString() { 106 return mContext.getString(mNameResourceId); 107 } 108 109 @Override accessProfileEnabled()110 public boolean accessProfileEnabled() { 111 return true; 112 } 113 114 @Override isAutoConnectable()115 public boolean isAutoConnectable() { 116 return true; 117 } 118 119 @Override getConnectionStatus(BluetoothDevice device)120 public int getConnectionStatus(BluetoothDevice device) { 121 if (mConnectedDevices.contains(device)) { 122 return BluetoothProfile.STATE_CONNECTED; 123 } else { 124 return BluetoothProfile.STATE_DISCONNECTED; 125 } 126 } 127 128 @Override isEnabled(BluetoothDevice device)129 public boolean isEnabled(BluetoothDevice device) { 130 return mPreferred.getOrDefault(device, false); 131 } 132 133 @Override getConnectionPolicy(BluetoothDevice device)134 public int getConnectionPolicy(BluetoothDevice device) { 135 return isEnabled(device) 136 ? BluetoothProfile.CONNECTION_POLICY_ALLOWED 137 : BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 138 } 139 140 @Override setEnabled(BluetoothDevice device, boolean enabled)141 public boolean setEnabled(BluetoothDevice device, boolean enabled) { 142 mPreferred.put(device, enabled); 143 return true; 144 } 145 146 @Override isProfileReady()147 public boolean isProfileReady() { 148 return true; 149 } 150 151 @Override getProfileId()152 public int getProfileId() { 153 return 0; 154 } 155 156 @Override getOrdinal()157 public int getOrdinal() { 158 return 0; 159 } 160 161 @Override getNameResource(BluetoothDevice device)162 public int getNameResource(BluetoothDevice device) { 163 return mNameResourceId; 164 } 165 166 @Override getSummaryResourceForDevice(BluetoothDevice device)167 public int getSummaryResourceForDevice(BluetoothDevice device) { 168 return Utils.getConnectionStateSummary(getConnectionStatus(device)); 169 } 170 171 @Override getDrawableResource(BluetoothClass btClass)172 public int getDrawableResource(BluetoothClass btClass) { 173 return 0; 174 } 175 } 176 177 /** 178 * Creates and adds a mock LocalBluetoothProfile to the list of connectable profiles for the 179 * device. 180 * @param profileNameResId the resource id for the name used by this profile 181 * @param deviceIsPreferred whether this profile should start out as enabled for the device 182 */ addFakeProfile(int profileNameResId, boolean deviceIsPreferred)183 private LocalBluetoothProfile addFakeProfile(int profileNameResId, 184 boolean deviceIsPreferred) { 185 LocalBluetoothProfile profile = new FakeBluetoothProfile(mContext, profileNameResId); 186 profile.setEnabled(mDevice, deviceIsPreferred); 187 mConnectableProfiles.add(profile); 188 when(mProfileManager.getProfileByName(eq(profile.toString()))).thenReturn(profile); 189 return profile; 190 } 191 192 /** Returns the list of SwitchPreference objects added to the screen - there should be one per 193 * Bluetooth profile. 194 */ getProfileSwitches(boolean expectOnlyMConnectable)195 private List<SwitchPreference> getProfileSwitches(boolean expectOnlyMConnectable) { 196 if (expectOnlyMConnectable) { 197 assertThat(mConnectableProfiles).isNotEmpty(); 198 assertThat(mProfiles.getPreferenceCount() - 1).isEqualTo(mConnectableProfiles.size()); 199 } 200 List<SwitchPreference> result = new ArrayList<>(); 201 for (int i = 0; i < mProfiles.getPreferenceCount(); i++) { 202 final Preference preference = mProfiles.getPreference(i); 203 if (preference instanceof SwitchPreference) { 204 result.add((SwitchPreference) preference); 205 } 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 = 213 mContext.getString(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(3); 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).isEnabled(mDevice)).isFalse(); 264 265 // Make sure no new preferences were added. 266 assertThat(mProfiles.getPreferenceCount()).isEqualTo(3); 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).isEnabled(mDevice)).isTrue(); 272 273 // Make sure we still haven't gotten any new preferences added. 274 assertThat(mProfiles.getPreferenceCount()).isEqualTo(3); 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 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 289 PbapServerProfile psp = mock(PbapServerProfile.class); 290 when(psp.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_pbap); 291 when(psp.toString()).thenReturn(PbapServerProfile.NAME); 292 when(psp.isProfileReady()).thenReturn(true); 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(2); 304 assertThat(mDevice.getPhonebookAccessPermission()) 305 .isEqualTo(BluetoothDevice.ACCESS_REJECTED); 306 } 307 308 @Test pbapProfileStartsDisabled()309 public void pbapProfileStartsDisabled() { 310 setupDevice(makeDefaultDeviceConfig()); 311 mDevice.setPhonebookAccessPermission(BluetoothDevice.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(psp.isProfileReady()).thenReturn(true); 316 when(mProfileManager.getPbapProfile()).thenReturn(psp); 317 318 showScreen(mController); 319 List<SwitchPreference> switches = getProfileSwitches(false); 320 assertThat(switches.size()).isEqualTo(1); 321 SwitchPreference pref = switches.get(0); 322 assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_pbap)); 323 assertThat(pref.isChecked()).isFalse(); 324 325 pref.performClick(); 326 assertThat(mProfiles.getPreferenceCount()).isEqualTo(2); 327 assertThat(mDevice.getPhonebookAccessPermission()) 328 .isEqualTo(BluetoothDevice.ACCESS_ALLOWED); 329 } 330 331 @Test mapProfile()332 public void mapProfile() { 333 setupDevice(makeDefaultDeviceConfig()); 334 MapProfile mapProfile = mock(MapProfile.class); 335 when(mapProfile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_map); 336 when(mapProfile.isProfileReady()).thenReturn(true); 337 when(mProfileManager.getMapProfile()).thenReturn(mapProfile); 338 when(mProfileManager.getProfileByName(eq(mapProfile.toString()))).thenReturn(mapProfile); 339 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 340 showScreen(mController); 341 List<SwitchPreference> switches = getProfileSwitches(false); 342 assertThat(switches.size()).isEqualTo(1); 343 SwitchPreference pref = switches.get(0); 344 assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_map)); 345 assertThat(pref.isChecked()).isFalse(); 346 347 pref.performClick(); 348 assertThat(mProfiles.getPreferenceCount()).isEqualTo(2); 349 assertThat(mDevice.getMessageAccessPermission()).isEqualTo(BluetoothDevice.ACCESS_ALLOWED); 350 } 351 addMockA2dpProfile(boolean preferred, boolean supportsHighQualityAudio, boolean highQualityAudioEnabled)352 private A2dpProfile addMockA2dpProfile(boolean preferred, boolean supportsHighQualityAudio, 353 boolean highQualityAudioEnabled) { 354 A2dpProfile profile = mock(A2dpProfile.class); 355 when(mProfileManager.getProfileByName(eq(profile.toString()))).thenReturn(profile); 356 when(profile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_a2dp); 357 when(profile.getHighQualityAudioOptionLabel(mDevice)).thenReturn( 358 mContext.getString(R.string.bluetooth_profile_a2dp_high_quality_unknown_codec)); 359 when(profile.supportsHighQualityAudio(mDevice)).thenReturn(supportsHighQualityAudio); 360 when(profile.isHighQualityAudioEnabled(mDevice)).thenReturn(highQualityAudioEnabled); 361 when(profile.isEnabled(mDevice)).thenReturn(preferred); 362 when(profile.isProfileReady()).thenReturn(true); 363 mConnectableProfiles.add(profile); 364 return profile; 365 } 366 getHighQualityAudioPref()367 private SwitchPreference getHighQualityAudioPref() { 368 return (SwitchPreference) mScreen.findPreference( 369 BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); 370 } 371 372 @Test highQualityAudio_prefIsPresentWhenSupported()373 public void highQualityAudio_prefIsPresentWhenSupported() { 374 setupDevice(makeDefaultDeviceConfig()); 375 addMockA2dpProfile(true, true, true); 376 showScreen(mController); 377 SwitchPreference pref = getHighQualityAudioPref(); 378 assertThat(pref.getKey()).isEqualTo( 379 BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); 380 381 // Make sure the preference works when clicked on. 382 pref.performClick(); 383 A2dpProfile profile = (A2dpProfile) mConnectableProfiles.get(0); 384 verify(profile).setHighQualityAudioEnabled(mDevice, false); 385 pref.performClick(); 386 verify(profile).setHighQualityAudioEnabled(mDevice, true); 387 } 388 389 @Test highQualityAudio_prefIsAbsentWhenNotSupported()390 public void highQualityAudio_prefIsAbsentWhenNotSupported() { 391 setupDevice(makeDefaultDeviceConfig()); 392 addMockA2dpProfile(true, false, false); 393 showScreen(mController); 394 assertThat(mProfiles.getPreferenceCount()).isEqualTo(2); 395 SwitchPreference pref = (SwitchPreference) mProfiles.getPreference(0); 396 assertThat(pref.getKey()) 397 .isNotEqualTo(BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG); 398 assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_a2dp)); 399 } 400 401 @Test highQualityAudio_busyDeviceDisablesSwitch()402 public void highQualityAudio_busyDeviceDisablesSwitch() { 403 setupDevice(makeDefaultDeviceConfig()); 404 addMockA2dpProfile(true, true, true); 405 when(mCachedDevice.isBusy()).thenReturn(true); 406 showScreen(mController); 407 SwitchPreference pref = getHighQualityAudioPref(); 408 assertThat(pref.isEnabled()).isFalse(); 409 } 410 411 @Test highQualityAudio_mediaAudioDisabledAndReEnabled()412 public void highQualityAudio_mediaAudioDisabledAndReEnabled() { 413 setupDevice(makeDefaultDeviceConfig()); 414 A2dpProfile audioProfile = addMockA2dpProfile(true, true, true); 415 showScreen(mController); 416 assertThat(mProfiles.getPreferenceCount()).isEqualTo(3); 417 418 // Disabling media audio should cause the high quality audio switch to disappear, but not 419 // the regular audio one. 420 SwitchPreference audioPref = 421 (SwitchPreference) mScreen.findPreference(audioProfile.toString()); 422 audioPref.performClick(); 423 verify(audioProfile).setEnabled(mDevice, false); 424 when(audioProfile.isEnabled(mDevice)).thenReturn(false); 425 mController.onDeviceAttributesChanged(); 426 assertThat(audioPref.isVisible()).isTrue(); 427 SwitchPreference highQualityAudioPref = getHighQualityAudioPref(); 428 assertThat(highQualityAudioPref.isVisible()).isFalse(); 429 430 // And re-enabling media audio should make high quality switch to reappear. 431 audioPref.performClick(); 432 verify(audioProfile).setEnabled(mDevice, true); 433 when(audioProfile.isEnabled(mDevice)).thenReturn(true); 434 mController.onDeviceAttributesChanged(); 435 highQualityAudioPref = getHighQualityAudioPref(); 436 assertThat(highQualityAudioPref.isVisible()).isTrue(); 437 } 438 439 @Test highQualityAudio_mediaAudioStartsDisabled()440 public void highQualityAudio_mediaAudioStartsDisabled() { 441 setupDevice(makeDefaultDeviceConfig()); 442 A2dpProfile audioProfile = addMockA2dpProfile(false, true, true); 443 showScreen(mController); 444 SwitchPreference audioPref = mScreen.findPreference(audioProfile.toString()); 445 SwitchPreference highQualityAudioPref = getHighQualityAudioPref(); 446 assertThat(audioPref).isNotNull(); 447 assertThat(audioPref.isChecked()).isFalse(); 448 assertThat(highQualityAudioPref).isNotNull(); 449 assertThat(highQualityAudioPref.isVisible()).isFalse(); 450 } 451 452 @Test onResume_addServiceListener()453 public void onResume_addServiceListener() { 454 mController.onResume(); 455 456 verify(mProfileManager).addServiceListener(mController); 457 } 458 459 @Test onPause_removeServiceListener()460 public void onPause_removeServiceListener() { 461 mController.onPause(); 462 463 verify(mProfileManager).removeServiceListener(mController); 464 } 465 } 466