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