1 /* 2 * Copyright (C) 2016 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 package com.android.settings.bluetooth; 17 18 import static com.google.common.truth.Truth.assertThat; 19 20 import static org.junit.Assert.fail; 21 import static org.mockito.ArgumentMatchers.any; 22 import static org.mockito.Mockito.doNothing; 23 import static org.mockito.Mockito.doReturn; 24 import static org.mockito.Mockito.mock; 25 import static org.mockito.Mockito.spy; 26 import static org.mockito.Mockito.times; 27 import static org.mockito.Mockito.verify; 28 import static org.mockito.Mockito.when; 29 30 import android.app.Dialog; 31 import android.content.Context; 32 import android.text.SpannableStringBuilder; 33 import android.text.TextUtils; 34 import android.view.View; 35 import android.view.inputmethod.InputMethodManager; 36 import android.widget.CheckBox; 37 import android.widget.TextView; 38 39 import androidx.appcompat.app.AlertDialog; 40 import androidx.fragment.app.FragmentActivity; 41 42 import com.android.settings.R; 43 import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; 44 45 import org.junit.Before; 46 import org.junit.Test; 47 import org.junit.runner.RunWith; 48 import org.mockito.Mock; 49 import org.mockito.MockitoAnnotations; 50 import org.robolectric.RobolectricTestRunner; 51 import org.robolectric.RuntimeEnvironment; 52 import org.robolectric.annotation.Config; 53 import org.robolectric.shadows.androidx.fragment.FragmentController; 54 55 @RunWith(RobolectricTestRunner.class) 56 @Config(shadows = ShadowAlertDialogCompat.class) 57 public class BluetoothPairingDialogTest { 58 59 private static final String FILLER = "text that goes in a view"; 60 private static final String FAKE_DEVICE_NAME = "Fake Bluetooth Device"; 61 62 @Mock 63 private BluetoothPairingController controller; 64 @Mock 65 private BluetoothPairingDialog dialogActivity; 66 67 @Before setUp()68 public void setUp() { 69 MockitoAnnotations.initMocks(this); 70 doNothing().when(dialogActivity).dismiss(); 71 } 72 73 @Test dialogUpdatesControllerWithUserInput()74 public void dialogUpdatesControllerWithUserInput() { 75 // set the correct dialog type 76 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 77 78 // we don't care about these for this test 79 when(controller.getDeviceVariantMessageId()) 80 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 81 when(controller.getDeviceVariantMessageHintId()) 82 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 83 84 // build fragment 85 BluetoothPairingDialogFragment frag = makeFragment(); 86 87 // test that controller is updated on text change 88 frag.afterTextChanged(new SpannableStringBuilder(FILLER)); 89 verify(controller, times(1)).updateUserInput(any()); 90 } 91 92 @Test dialogEnablesSubmitButtonOnValidationFromController()93 public void dialogEnablesSubmitButtonOnValidationFromController() { 94 // set the correct dialog type 95 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 96 97 // we don't care about these for this test 98 when(controller.getDeviceVariantMessageId()) 99 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 100 when(controller.getDeviceVariantMessageHintId()) 101 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 102 103 // force the controller to say that any passkey is valid 104 when(controller.isPasskeyValid(any())).thenReturn(true); 105 106 // build fragment 107 BluetoothPairingDialogFragment frag = makeFragment(); 108 109 // test that the positive button is enabled when passkey is valid 110 frag.afterTextChanged(new SpannableStringBuilder(FILLER)); 111 View button = frag.getmDialog().getButton(AlertDialog.BUTTON_POSITIVE); 112 assertThat(button).isNotNull(); 113 assertThat(button.getVisibility()).isEqualTo(View.VISIBLE); 114 } 115 116 @Test dialogDoesNotAskForPairCodeOnConsentVariant()117 public void dialogDoesNotAskForPairCodeOnConsentVariant() { 118 // set the dialog variant to confirmation/consent 119 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 120 121 // build the fragment 122 BluetoothPairingDialogFragment frag = makeFragment(); 123 124 // check that the input field used by the entry dialog fragment does not exist 125 View view = frag.getmDialog().findViewById(R.id.text); 126 assertThat(view).isNull(); 127 } 128 129 @Test dialogAsksForPairCodeOnUserEntryVariant()130 public void dialogAsksForPairCodeOnUserEntryVariant() { 131 // set the dialog variant to user entry 132 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 133 134 // we don't care about these for this test 135 when(controller.getDeviceVariantMessageId()) 136 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 137 when(controller.getDeviceVariantMessageHintId()) 138 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 139 140 Context context = spy(RuntimeEnvironment.application); 141 InputMethodManager imm = mock(InputMethodManager.class); 142 doReturn(imm).when(context).getSystemService(Context.INPUT_METHOD_SERVICE); 143 144 // build the fragment 145 BluetoothPairingDialogFragment frag = spy(new BluetoothPairingDialogFragment()); 146 when(frag.getContext()).thenReturn(context); 147 setupFragment(frag); 148 AlertDialog alertDialog = frag.getmDialog(); 149 150 // check that the pin/passkey input field is visible to the user 151 View view = alertDialog.findViewById(R.id.text); 152 assertThat(view.getVisibility()).isEqualTo(View.VISIBLE); 153 154 // check that showSoftInput was called to make input method appear when the dialog was shown 155 assertThat(view.isFocused()).isTrue(); 156 // TODO(b/73892004): Figure out why this is failing. 157 // assertThat(imm.isActive()).isTrue(); 158 verify(imm).showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); 159 } 160 161 @Test dialogDisplaysPairCodeOnDisplayPasskeyVariant()162 public void dialogDisplaysPairCodeOnDisplayPasskeyVariant() { 163 // set the dialog variant to display passkey 164 when(controller.getDialogType()) 165 .thenReturn(BluetoothPairingController.DISPLAY_PASSKEY_DIALOG); 166 167 // ensure that the controller returns good values to indicate a passkey needs to be shown 168 when(controller.isDisplayPairingKeyVariant()).thenReturn(true); 169 when(controller.hasPairingContent()).thenReturn(true); 170 when(controller.getPairingContent()).thenReturn(FILLER); 171 172 // build the fragment 173 BluetoothPairingDialogFragment frag = makeFragment(); 174 175 // get the relevant views 176 View messagePairing = frag.getmDialog().findViewById(R.id.pairing_code_message); 177 TextView pairingViewContent = frag.getmDialog().findViewById(R.id.pairing_subhead); 178 View pairingViewCaption = frag.getmDialog().findViewById(R.id.pairing_caption); 179 180 // check that the relevant views are visible and that the passkey is shown 181 assertThat(messagePairing.getVisibility()).isEqualTo(View.VISIBLE); 182 assertThat(pairingViewCaption.getVisibility()).isEqualTo(View.VISIBLE); 183 assertThat(pairingViewContent.getVisibility()).isEqualTo(View.VISIBLE); 184 assertThat(TextUtils.equals(FILLER, pairingViewContent.getText())).isTrue(); 185 } 186 187 @Test(expected = IllegalStateException.class) dialogThrowsExceptionIfNoControllerSet()188 public void dialogThrowsExceptionIfNoControllerSet() { 189 // instantiate a fragment 190 BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment(); 191 192 // this should throw an error 193 FragmentController.setupFragment(frag, FragmentActivity.class, 0 /* containerViewId */, 194 null /* bundle */); 195 fail("Starting the fragment with no controller set should have thrown an exception."); 196 } 197 198 @Test dialogCallsHookOnPositiveButtonPress()199 public void dialogCallsHookOnPositiveButtonPress() { 200 // set the dialog variant to confirmation/consent 201 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 202 203 // we don't care what this does, just that it is called 204 doNothing().when(controller).onDialogPositiveClick(any()); 205 206 // build the fragment 207 BluetoothPairingDialogFragment frag = makeFragment(); 208 209 // click the button and verify that the controller hook was called 210 frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE); 211 verify(controller, times(1)).onDialogPositiveClick(any()); 212 } 213 214 @Test dialogCallsHookOnNegativeButtonPress()215 public void dialogCallsHookOnNegativeButtonPress() { 216 // set the dialog variant to confirmation/consent 217 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 218 219 // we don't care what this does, just that it is called 220 doNothing().when(controller).onDialogNegativeClick(any()); 221 222 // build the fragment 223 BluetoothPairingDialogFragment frag = makeFragment(); 224 225 // click the button and verify that the controller hook was called 226 frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE); 227 verify(controller, times(1)).onDialogNegativeClick(any()); 228 } 229 230 @Test(expected = IllegalStateException.class) dialogDoesNotAllowSwappingController()231 public void dialogDoesNotAllowSwappingController() { 232 // instantiate a fragment 233 BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment(); 234 frag.setPairingController(controller); 235 236 // this should throw an error 237 frag.setPairingController(controller); 238 fail("Setting the controller multiple times should throw an exception."); 239 } 240 241 @Test(expected = IllegalStateException.class) dialogDoesNotAllowSwappingActivity()242 public void dialogDoesNotAllowSwappingActivity() { 243 // instantiate a fragment 244 BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment(); 245 frag.setPairingDialogActivity(dialogActivity); 246 247 // this should throw an error 248 frag.setPairingDialogActivity(dialogActivity); 249 fail("Setting the dialog activity multiple times should throw an exception."); 250 } 251 252 @Test dialogPositiveButtonDisabledWhenUserInputInvalid()253 public void dialogPositiveButtonDisabledWhenUserInputInvalid() { 254 // set the correct dialog type 255 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 256 257 // we don't care about these for this test 258 when(controller.getDeviceVariantMessageId()) 259 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 260 when(controller.getDeviceVariantMessageHintId()) 261 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 262 263 // force the controller to say that any passkey is valid 264 when(controller.isPasskeyValid(any())).thenReturn(false); 265 266 // build fragment 267 BluetoothPairingDialogFragment frag = makeFragment(); 268 269 // test that the positive button is enabled when passkey is valid 270 frag.afterTextChanged(new SpannableStringBuilder(FILLER)); 271 View button = frag.getmDialog().getButton(AlertDialog.BUTTON_POSITIVE); 272 assertThat(button).isNotNull(); 273 assertThat(button.isEnabled()).isFalse(); 274 } 275 276 @Test dialogShowsContactSharingCheckboxWhenBluetoothProfileNotReady()277 public void dialogShowsContactSharingCheckboxWhenBluetoothProfileNotReady() { 278 // set the dialog variant to confirmation/consent 279 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 280 281 // set a fake device name and pretend the profile has not been set up for it 282 when(controller.getDeviceName()).thenReturn(FAKE_DEVICE_NAME); 283 when(controller.isProfileReady()).thenReturn(false); 284 285 // build the fragment 286 BluetoothPairingDialogFragment frag = makeFragment(); 287 288 // verify that the checkbox is visible and that the device name is correct 289 CheckBox sharingCheckbox = 290 frag.getmDialog().findViewById(R.id.phonebook_sharing_message_confirm_pin); 291 assertThat(sharingCheckbox.getVisibility()).isEqualTo(View.VISIBLE); 292 } 293 294 @Test dialogHidesContactSharingCheckboxWhenBluetoothProfileIsReady()295 public void dialogHidesContactSharingCheckboxWhenBluetoothProfileIsReady() { 296 // set the dialog variant to confirmation/consent 297 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 298 299 // set a fake device name and pretend the profile has been set up for it 300 when(controller.getDeviceName()).thenReturn(FAKE_DEVICE_NAME); 301 when(controller.isProfileReady()).thenReturn(true); 302 303 // build the fragment 304 BluetoothPairingDialogFragment frag = makeFragment(); 305 306 // verify that the checkbox is gone 307 CheckBox sharingCheckbox = 308 frag.getmDialog().findViewById(R.id.phonebook_sharing_message_confirm_pin); 309 assertThat(sharingCheckbox.getVisibility()).isEqualTo(View.GONE); 310 } 311 312 @Test dialogShowsMessageOnPinEntryView()313 public void dialogShowsMessageOnPinEntryView() { 314 // set the correct dialog type 315 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 316 317 // Set the message id to something specific to verify later 318 when(controller.getDeviceVariantMessageId()).thenReturn(R.string.cancel); 319 when(controller.getDeviceVariantMessageHintId()) 320 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 321 322 // build the fragment 323 BluetoothPairingDialogFragment frag = makeFragment(); 324 325 // verify message is what we expect it to be and is visible 326 TextView message = frag.getmDialog().findViewById(R.id.message_below_pin); 327 assertThat(message.getVisibility()).isEqualTo(View.VISIBLE); 328 assertThat(TextUtils.equals(frag.getString(R.string.cancel), message.getText())).isTrue(); 329 } 330 331 @Test dialogShowsMessageHintOnPinEntryView()332 public void dialogShowsMessageHintOnPinEntryView() { 333 // set the correct dialog type 334 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 335 336 // Set the message id hint to something specific to verify later 337 when(controller.getDeviceVariantMessageHintId()).thenReturn(R.string.cancel); 338 when(controller.getDeviceVariantMessageId()) 339 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 340 341 // build the fragment 342 BluetoothPairingDialogFragment frag = makeFragment(); 343 344 // verify message is what we expect it to be and is visible 345 TextView hint = frag.getmDialog().findViewById(R.id.pin_values_hint); 346 assertThat(hint.getVisibility()).isEqualTo(View.VISIBLE); 347 assertThat(TextUtils.equals(frag.getString(R.string.cancel), hint.getText())).isTrue(); 348 } 349 350 @Test dialogHidesMessageAndHintWhenNotProvidedOnPinEntryView()351 public void dialogHidesMessageAndHintWhenNotProvidedOnPinEntryView() { 352 // set the correct dialog type 353 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 354 355 // Set the id's to what is returned when it is not provided 356 when(controller.getDeviceVariantMessageHintId()) 357 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 358 when(controller.getDeviceVariantMessageId()) 359 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 360 361 // build the fragment 362 BluetoothPairingDialogFragment frag = makeFragment(); 363 364 // verify message is what we expect it to be and is visible 365 TextView hint = frag.getmDialog().findViewById(R.id.pin_values_hint); 366 assertThat(hint.getVisibility()).isEqualTo(View.GONE); 367 TextView message = frag.getmDialog().findViewById(R.id.message_below_pin); 368 assertThat(message.getVisibility()).isEqualTo(View.GONE); 369 } 370 371 @Test pairingStringIsFormattedCorrectly()372 public void pairingStringIsFormattedCorrectly() { 373 final String device = "test_device"; 374 final Context context = RuntimeEnvironment.application; 375 assertThat(context.getString(R.string.bluetooth_pb_acceptance_dialog_text, device, device)) 376 .contains(device); 377 } 378 379 @Test pairingDialogDismissedOnPositiveClick()380 public void pairingDialogDismissedOnPositiveClick() { 381 // set the dialog variant to confirmation/consent 382 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 383 384 // we don't care what this does, just that it is called 385 doNothing().when(controller).onDialogPositiveClick(any()); 386 387 // build the fragment 388 BluetoothPairingDialogFragment frag = makeFragment(); 389 390 // click the button and verify that the controller hook was called 391 frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE); 392 393 verify(controller, times(1)).onDialogPositiveClick(any()); 394 verify(dialogActivity, times(1)).dismiss(); 395 } 396 397 @Test pairingDialogDismissedOnNegativeClick()398 public void pairingDialogDismissedOnNegativeClick() { 399 // set the dialog variant to confirmation/consent 400 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 401 402 // we don't care what this does, just that it is called 403 doNothing().when(controller).onDialogNegativeClick(any()); 404 405 // build the fragment 406 BluetoothPairingDialogFragment frag = makeFragment(); 407 408 // click the button and verify that the controller hook was called 409 frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE); 410 411 verify(controller, times(1)).onDialogNegativeClick(any()); 412 verify(dialogActivity, times(1)).dismiss(); 413 } 414 415 @Test rotateDialog_nullPinText_okButtonEnabled()416 public void rotateDialog_nullPinText_okButtonEnabled() { 417 userEntryDialogExistingTextTest(null); 418 } 419 420 @Test rotateDialog_emptyPinText_okButtonEnabled()421 public void rotateDialog_emptyPinText_okButtonEnabled() { 422 userEntryDialogExistingTextTest(""); 423 } 424 425 @Test rotateDialog_nonEmptyPinText_okButtonEnabled()426 public void rotateDialog_nonEmptyPinText_okButtonEnabled() { 427 userEntryDialogExistingTextTest("test"); 428 } 429 430 // Runs a test simulating the user entry dialog type in a situation like device rotation, where 431 // the dialog fragment gets created and we already have some existing text entered into the 432 // pin field. userEntryDialogExistingTextTest(CharSequence existingText)433 private void userEntryDialogExistingTextTest(CharSequence existingText) { 434 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 435 when(controller.getDeviceVariantMessageHintId()) 436 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 437 when(controller.getDeviceVariantMessageId()) 438 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 439 440 BluetoothPairingDialogFragment fragment = spy(new BluetoothPairingDialogFragment()); 441 when(fragment.getPairingViewText()).thenReturn(existingText); 442 setupFragment(fragment); 443 AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); 444 assertThat(dialog).isNotNull(); 445 boolean expected = !TextUtils.isEmpty(existingText); 446 assertThat(dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled()).isEqualTo(expected); 447 } 448 setupFragment(BluetoothPairingDialogFragment frag)449 private void setupFragment(BluetoothPairingDialogFragment frag) { 450 assertThat(frag.isPairingControllerSet()).isFalse(); 451 frag.setPairingController(controller); 452 assertThat(frag.isPairingDialogActivitySet()).isFalse(); 453 frag.setPairingDialogActivity(dialogActivity); 454 FragmentController.setupFragment(frag, FragmentActivity.class, 0 /* containerViewId */, 455 null /* bundle */); 456 assertThat(frag.getmDialog()).isNotNull(); 457 assertThat(frag.isPairingControllerSet()).isTrue(); 458 assertThat(frag.isPairingDialogActivitySet()).isTrue(); 459 } 460 makeFragment()461 private BluetoothPairingDialogFragment makeFragment() { 462 BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment(); 463 setupFragment(frag); 464 return frag; 465 } 466 } 467