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.TextView; 37 38 import androidx.appcompat.app.AlertDialog; 39 import androidx.fragment.app.FragmentActivity; 40 41 import com.android.settings.R; 42 import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; 43 44 import org.junit.Before; 45 import org.junit.Ignore; 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 @Ignore 130 @Test dialogAsksForPairCodeOnUserEntryVariant()131 public void dialogAsksForPairCodeOnUserEntryVariant() { 132 // set the dialog variant to user entry 133 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 134 135 // we don't care about these for this test 136 when(controller.getDeviceVariantMessageId()) 137 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 138 when(controller.getDeviceVariantMessageHintId()) 139 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 140 141 Context context = spy(RuntimeEnvironment.application); 142 InputMethodManager imm = mock(InputMethodManager.class); 143 doReturn(imm).when(context).getSystemService(Context.INPUT_METHOD_SERVICE); 144 145 // build the fragment 146 BluetoothPairingDialogFragment frag = spy(new BluetoothPairingDialogFragment()); 147 when(frag.getContext()).thenReturn(context); 148 setupFragment(frag); 149 AlertDialog alertDialog = frag.getmDialog(); 150 151 // check that the pin/passkey input field is visible to the user 152 View view = alertDialog.findViewById(R.id.text); 153 assertThat(view.getVisibility()).isEqualTo(View.VISIBLE); 154 155 // check that showSoftInput was called to make input method appear when the dialog was shown 156 assertThat(view.isFocused()).isTrue(); 157 // TODO(b/73892004): Figure out why this is failing. 158 // assertThat(imm.isActive()).isTrue(); 159 verify(imm).showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); 160 } 161 162 @Test dialogDisplaysPairCodeOnDisplayPasskeyVariant()163 public void dialogDisplaysPairCodeOnDisplayPasskeyVariant() { 164 // set the dialog variant to display passkey 165 when(controller.getDialogType()) 166 .thenReturn(BluetoothPairingController.DISPLAY_PASSKEY_DIALOG); 167 168 // ensure that the controller returns good values to indicate a passkey needs to be shown 169 when(controller.isDisplayPairingKeyVariant()).thenReturn(true); 170 when(controller.hasPairingContent()).thenReturn(true); 171 when(controller.getPairingContent()).thenReturn(FILLER); 172 173 // build the fragment 174 BluetoothPairingDialogFragment frag = makeFragment(); 175 176 // get the relevant views 177 View messagePairing = frag.getmDialog().findViewById(R.id.pairing_code_message); 178 TextView pairingViewContent = frag.getmDialog().findViewById(R.id.pairing_subhead); 179 View pairingViewCaption = frag.getmDialog().findViewById(R.id.pairing_caption); 180 181 // check that the relevant views are visible and that the passkey is shown 182 assertThat(messagePairing.getVisibility()).isEqualTo(View.VISIBLE); 183 assertThat(pairingViewCaption.getVisibility()).isEqualTo(View.VISIBLE); 184 assertThat(pairingViewContent.getVisibility()).isEqualTo(View.VISIBLE); 185 assertThat(TextUtils.equals(FILLER, pairingViewContent.getText())).isTrue(); 186 } 187 188 @Test(expected = IllegalStateException.class) dialogThrowsExceptionIfNoControllerSet()189 public void dialogThrowsExceptionIfNoControllerSet() { 190 // instantiate a fragment 191 BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment(); 192 193 // this should throw an error 194 FragmentController.setupFragment(frag, FragmentActivity.class, 0 /* containerViewId */, 195 null /* bundle */); 196 fail("Starting the fragment with no controller set should have thrown an exception."); 197 } 198 199 @Test dialogCallsHookOnPositiveButtonPress()200 public void dialogCallsHookOnPositiveButtonPress() { 201 // set the dialog variant to confirmation/consent 202 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 203 204 // we don't care what this does, just that it is called 205 doNothing().when(controller).onDialogPositiveClick(any()); 206 207 // build the fragment 208 BluetoothPairingDialogFragment frag = makeFragment(); 209 210 // click the button and verify that the controller hook was called 211 frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE); 212 verify(controller, times(1)).onDialogPositiveClick(any()); 213 } 214 215 @Test dialogCallsHookOnNegativeButtonPress()216 public void dialogCallsHookOnNegativeButtonPress() { 217 // set the dialog variant to confirmation/consent 218 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 219 220 // we don't care what this does, just that it is called 221 doNothing().when(controller).onDialogNegativeClick(any()); 222 223 // build the fragment 224 BluetoothPairingDialogFragment frag = makeFragment(); 225 226 // click the button and verify that the controller hook was called 227 frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE); 228 verify(controller, times(1)).onDialogNegativeClick(any()); 229 } 230 231 @Test(expected = IllegalStateException.class) dialogDoesNotAllowSwappingController()232 public void dialogDoesNotAllowSwappingController() { 233 // instantiate a fragment 234 BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment(); 235 frag.setPairingController(controller); 236 237 // this should throw an error 238 frag.setPairingController(controller); 239 fail("Setting the controller multiple times should throw an exception."); 240 } 241 242 @Test(expected = IllegalStateException.class) dialogDoesNotAllowSwappingActivity()243 public void dialogDoesNotAllowSwappingActivity() { 244 // instantiate a fragment 245 BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment(); 246 frag.setPairingDialogActivity(dialogActivity); 247 248 // this should throw an error 249 frag.setPairingDialogActivity(dialogActivity); 250 fail("Setting the dialog activity multiple times should throw an exception."); 251 } 252 253 @Test dialogPositiveButtonDisabledWhenUserInputInvalid()254 public void dialogPositiveButtonDisabledWhenUserInputInvalid() { 255 // set the correct dialog type 256 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 257 258 // we don't care about these for this test 259 when(controller.getDeviceVariantMessageId()) 260 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 261 when(controller.getDeviceVariantMessageHintId()) 262 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 263 264 // force the controller to say that any passkey is valid 265 when(controller.isPasskeyValid(any())).thenReturn(false); 266 267 // build fragment 268 BluetoothPairingDialogFragment frag = makeFragment(); 269 270 // test that the positive button is enabled when passkey is valid 271 frag.afterTextChanged(new SpannableStringBuilder(FILLER)); 272 View button = frag.getmDialog().getButton(AlertDialog.BUTTON_POSITIVE); 273 assertThat(button).isNotNull(); 274 assertThat(button.isEnabled()).isFalse(); 275 } 276 277 @Test contactSharingToggle_conditionIsReady_showsUi()278 public void contactSharingToggle_conditionIsReady_showsUi() { 279 // set the dialog variant to confirmation/consent 280 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 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.isContactSharingVisible()).thenReturn(true); 284 285 // build the fragment 286 BluetoothPairingDialogFragment frag = makeFragment(); 287 288 // verify that the toggle is visible 289 View sharingToggle = 290 frag.getmDialog().findViewById(R.id.phonebook_sharing); 291 assertThat(sharingToggle.getVisibility()).isEqualTo(View.VISIBLE); 292 } 293 294 @Test contactSharingToggle_conditionIsNotReady_doesNotShowUi()295 public void contactSharingToggle_conditionIsNotReady_doesNotShowUi() { 296 // set the dialog variant to confirmation/consent 297 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 298 // set a fake device name and pretend the profile has been set up for it 299 when(controller.getDeviceName()).thenReturn(FAKE_DEVICE_NAME); 300 when(controller.isContactSharingVisible()).thenReturn(false); 301 302 // build the fragment 303 BluetoothPairingDialogFragment frag = makeFragment(); 304 305 // verify that the toggle is gone 306 View sharingToggle = 307 frag.getmDialog().findViewById(R.id.phonebook_sharing); 308 assertThat(sharingToggle.getVisibility()).isEqualTo(View.GONE); 309 } 310 311 @Test dialogShowsMessageOnPinEntryView()312 public void dialogShowsMessageOnPinEntryView() { 313 // set the correct dialog type 314 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 315 316 // Set the message id to something specific to verify later 317 when(controller.getDeviceVariantMessageId()).thenReturn(R.string.cancel); 318 when(controller.getDeviceVariantMessageHintId()) 319 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 320 321 // build the fragment 322 BluetoothPairingDialogFragment frag = makeFragment(); 323 324 // verify message is what we expect it to be and is visible 325 TextView message = frag.getmDialog().findViewById(R.id.message_below_pin); 326 assertThat(message.getVisibility()).isEqualTo(View.VISIBLE); 327 assertThat(TextUtils.equals(frag.getString(R.string.cancel), message.getText())).isTrue(); 328 } 329 330 @Test dialogShowsMessageHintOnPinEntryView()331 public void dialogShowsMessageHintOnPinEntryView() { 332 // set the correct dialog type 333 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 334 335 // Set the message id hint to something specific to verify later 336 when(controller.getDeviceVariantMessageHintId()).thenReturn(R.string.cancel); 337 when(controller.getDeviceVariantMessageId()) 338 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 339 340 // build the fragment 341 BluetoothPairingDialogFragment frag = makeFragment(); 342 343 // verify message is what we expect it to be and is visible 344 TextView hint = frag.getmDialog().findViewById(R.id.pin_values_hint); 345 assertThat(hint.getVisibility()).isEqualTo(View.VISIBLE); 346 assertThat(TextUtils.equals(frag.getString(R.string.cancel), hint.getText())).isTrue(); 347 } 348 349 @Test dialogHidesMessageAndHintWhenNotProvidedOnPinEntryView()350 public void dialogHidesMessageAndHintWhenNotProvidedOnPinEntryView() { 351 // set the correct dialog type 352 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 353 354 // Set the id's to what is returned when it is not provided 355 when(controller.getDeviceVariantMessageHintId()) 356 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 357 when(controller.getDeviceVariantMessageId()) 358 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 359 360 // build the fragment 361 BluetoothPairingDialogFragment frag = makeFragment(); 362 363 // verify message is what we expect it to be and is visible 364 TextView hint = frag.getmDialog().findViewById(R.id.pin_values_hint); 365 assertThat(hint.getVisibility()).isEqualTo(View.GONE); 366 TextView message = frag.getmDialog().findViewById(R.id.message_below_pin); 367 assertThat(message.getVisibility()).isEqualTo(View.GONE); 368 } 369 370 @Test pairingDialogDismissedOnPositiveClick()371 public void pairingDialogDismissedOnPositiveClick() { 372 // set the dialog variant to confirmation/consent 373 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 374 375 // we don't care what this does, just that it is called 376 doNothing().when(controller).onDialogPositiveClick(any()); 377 378 // build the fragment 379 BluetoothPairingDialogFragment frag = makeFragment(); 380 381 // click the button and verify that the controller hook was called 382 frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE); 383 384 verify(controller, times(1)).onDialogPositiveClick(any()); 385 verify(dialogActivity, times(1)).dismiss(); 386 } 387 388 @Test pairingDialogDismissedOnNegativeClick()389 public void pairingDialogDismissedOnNegativeClick() { 390 // set the dialog variant to confirmation/consent 391 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 392 393 // we don't care what this does, just that it is called 394 doNothing().when(controller).onDialogNegativeClick(any()); 395 396 // build the fragment 397 BluetoothPairingDialogFragment frag = makeFragment(); 398 399 // click the button and verify that the controller hook was called 400 frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE); 401 402 verify(controller, times(1)).onDialogNegativeClick(any()); 403 verify(dialogActivity, times(1)).dismiss(); 404 } 405 406 @Ignore 407 @Test rotateDialog_nullPinText_okButtonEnabled()408 public void rotateDialog_nullPinText_okButtonEnabled() { 409 userEntryDialogExistingTextTest(null); 410 } 411 412 @Ignore 413 @Test rotateDialog_emptyPinText_okButtonEnabled()414 public void rotateDialog_emptyPinText_okButtonEnabled() { 415 userEntryDialogExistingTextTest(""); 416 } 417 418 @Ignore 419 @Test rotateDialog_nonEmptyPinText_okButtonEnabled()420 public void rotateDialog_nonEmptyPinText_okButtonEnabled() { 421 userEntryDialogExistingTextTest("test"); 422 } 423 424 @Test groupPairing_setMemberDevice_showsMessageHint()425 public void groupPairing_setMemberDevice_showsMessageHint() { 426 // set the correct dialog type 427 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 428 when(controller.isCoordinatedSetMemberDevice()).thenReturn(true); 429 430 // build the fragment 431 BluetoothPairingDialogFragment frag = makeFragment(); 432 433 // verify message is what we expect it to be and is visible 434 TextView message = frag.getmDialog().findViewById(R.id.pairing_group_message); 435 assertThat(message.getVisibility()).isEqualTo(View.VISIBLE); 436 } 437 438 @Test groupPairing_nonSetMemberDevice_hidesMessageHint()439 public void groupPairing_nonSetMemberDevice_hidesMessageHint() { 440 // set the correct dialog type 441 when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG); 442 when(controller.isCoordinatedSetMemberDevice()).thenReturn(false); 443 444 // build the fragment 445 BluetoothPairingDialogFragment frag = makeFragment(); 446 447 // verify message is what we expect it to be and is visible 448 TextView message = frag.getmDialog().findViewById(R.id.pairing_group_message); 449 assertThat(message.getVisibility()).isEqualTo(View.GONE); 450 } 451 452 // Runs a test simulating the user entry dialog type in a situation like device rotation, where 453 // the dialog fragment gets created and we already have some existing text entered into the 454 // pin field. userEntryDialogExistingTextTest(CharSequence existingText)455 private void userEntryDialogExistingTextTest(CharSequence existingText) { 456 when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG); 457 when(controller.getDeviceVariantMessageHintId()) 458 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 459 when(controller.getDeviceVariantMessageId()) 460 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE); 461 462 BluetoothPairingDialogFragment fragment = spy(new BluetoothPairingDialogFragment()); 463 when(fragment.getPairingViewText()).thenReturn(existingText); 464 setupFragment(fragment); 465 AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); 466 assertThat(dialog).isNotNull(); 467 boolean expected = !TextUtils.isEmpty(existingText); 468 assertThat(dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled()).isEqualTo(expected); 469 } 470 setupFragment(BluetoothPairingDialogFragment frag)471 private void setupFragment(BluetoothPairingDialogFragment frag) { 472 assertThat(frag.isPairingControllerSet()).isFalse(); 473 frag.setPairingController(controller); 474 assertThat(frag.isPairingDialogActivitySet()).isFalse(); 475 frag.setPairingDialogActivity(dialogActivity); 476 FragmentController.setupFragment(frag, FragmentActivity.class, 0 /* containerViewId */, 477 null /* bundle */); 478 assertThat(frag.getmDialog()).isNotNull(); 479 assertThat(frag.isPairingControllerSet()).isTrue(); 480 assertThat(frag.isPairingDialogActivitySet()).isTrue(); 481 } 482 makeFragment()483 private BluetoothPairingDialogFragment makeFragment() { 484 BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment(); 485 setupFragment(frag); 486 return frag; 487 } 488 } 489