Bluetooth: Dismiss pairing dialog on user click

* Existing pairing dialog should be dismissed when user clicks on Yes/No
* In a pairing session with multiple pairing dialogs, this is necessary
  as otherwise the second pairing dialog will not be shown
* Modified unit test to test this behavior
* Launch pairing dialog as UserHandle.CURRENT to avoid Context warnings

Bug: 35833536
Test: make, unit test, pair with Bluetooth devices
Change-Id: I1823b78d287134505f59eab7caca2329ecc3a36f
This commit is contained in:
Jack He
2017-04-25 12:53:33 -07:00
parent d796fda4bf
commit 2a67cf0465
4 changed files with 118 additions and 18 deletions

View File

@@ -24,16 +24,17 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.VisibleForTesting;
/** /**
* BluetoothPairingDialog asks the user to enter a PIN / Passkey / simple confirmation * BluetoothPairingDialog asks the user to enter a PIN / Passkey / simple confirmation
* for pairing with a remote Bluetooth device. It is an activity that appears as a dialog. * for pairing with a remote Bluetooth device. It is an activity that appears as a dialog.
*/ */
public final class BluetoothPairingDialog extends Activity { public class BluetoothPairingDialog extends Activity {
public static final String FRAGMENT_TAG = "bluetooth.pairing.fragment"; public static final String FRAGMENT_TAG = "bluetooth.pairing.fragment";
private BluetoothPairingController mBluetoothPairingController; private BluetoothPairingController mBluetoothPairingController;
private boolean mReceiverRegistered; private boolean mReceiverRegistered = false;
/** /**
* Dismiss the dialog if the bond state changes to bonded or none, * Dismiss the dialog if the bond state changes to bonded or none,
@@ -62,23 +63,26 @@ public final class BluetoothPairingDialog extends Activity {
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
boolean fragmentFound = true;
BluetoothPairingDialogFragment bluetoothFragment =
(BluetoothPairingDialogFragment) getFragmentManager()
.findFragmentByTag(FRAGMENT_TAG);
Intent intent = getIntent(); Intent intent = getIntent();
mBluetoothPairingController = new BluetoothPairingController(intent, this); mBluetoothPairingController = new BluetoothPairingController(intent, this);
// build the dialog fragment
// check if the fragment exists already boolean fragmentFound = true;
// check if the fragment has been preloaded
BluetoothPairingDialogFragment bluetoothFragment =
(BluetoothPairingDialogFragment) getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
// dismiss the fragment if it is already used
if (bluetoothFragment != null && (bluetoothFragment.isPairingControllerSet()
|| bluetoothFragment.isPairingDialogActivitySet())) {
bluetoothFragment.dismiss();
bluetoothFragment = null;
}
// build a new fragment if it is null
if (bluetoothFragment == null) { if (bluetoothFragment == null) {
fragmentFound = false; fragmentFound = false;
bluetoothFragment = new BluetoothPairingDialogFragment(); bluetoothFragment = new BluetoothPairingDialogFragment();
} }
// set the controller
bluetoothFragment.setPairingController(mBluetoothPairingController); bluetoothFragment.setPairingController(mBluetoothPairingController);
bluetoothFragment.setPairingDialogActivity(this);
// pass the fragment to the manager when it is created from scratch // pass the fragment to the manager when it is created from scratch
if (!fragmentFound) { if (!fragmentFound) {
bluetoothFragment.show(getFragmentManager(), FRAGMENT_TAG); bluetoothFragment.show(getFragmentManager(), FRAGMENT_TAG);
@@ -101,8 +105,15 @@ public final class BluetoothPairingDialog extends Activity {
} }
} }
private void dismiss() { @VisibleForTesting
void dismiss() {
if (!isFinishing()) { if (!isFinishing()) {
BluetoothPairingDialogFragment bluetoothFragment =
(BluetoothPairingDialogFragment) getFragmentManager()
.findFragmentByTag(FRAGMENT_TAG);
if (bluetoothFragment != null) {
bluetoothFragment.dismiss();
}
finish(); finish();
} }
} }

View File

@@ -47,6 +47,7 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
private AlertDialog.Builder mBuilder; private AlertDialog.Builder mBuilder;
private AlertDialog mDialog; private AlertDialog mDialog;
private BluetoothPairingController mPairingController; private BluetoothPairingController mPairingController;
private BluetoothPairingDialog mPairingDialogActivity;
private EditText mPairingView; private EditText mPairingView;
/** /**
* The interface we expect a listener to implement. Typically this should be done by * The interface we expect a listener to implement. Typically this should be done by
@@ -61,9 +62,13 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
if (mPairingController == null) { if (!isPairingControllerSet()) {
throw new IllegalStateException( throw new IllegalStateException(
"Must call setPairingController() before showing dialog"); "Must call setPairingController() before showing dialog");
}
if (!isPairingDialogActivitySet()) {
throw new IllegalStateException(
"Must call setPairingDialogActivity() before showing dialog");
} }
mBuilder = new AlertDialog.Builder(getActivity()); mBuilder = new AlertDialog.Builder(getActivity());
mDialog = setupDialog(); mDialog = setupDialog();
@@ -97,6 +102,7 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
} else if (which == DialogInterface.BUTTON_NEGATIVE) { } else if (which == DialogInterface.BUTTON_NEGATIVE) {
mPairingController.onDialogNegativeClick(this); mPairingController.onDialogNegativeClick(this);
} }
mPairingDialogActivity.dismiss();
} }
@Override @Override
@@ -119,14 +125,41 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
* controller may not be substituted once it is assigned. Forcibly switching a * controller may not be substituted once it is assigned. Forcibly switching a
* controller for a new one will lead to undefined behavior. * controller for a new one will lead to undefined behavior.
*/ */
public void setPairingController(BluetoothPairingController pairingController) { void setPairingController(BluetoothPairingController pairingController) {
if (mPairingController != null) { if (isPairingControllerSet()) {
throw new IllegalStateException("The controller can only be set once. " throw new IllegalStateException("The controller can only be set once. "
+ "Forcibly replacing it will lead to undefined behavior"); + "Forcibly replacing it will lead to undefined behavior");
} }
mPairingController = pairingController; mPairingController = pairingController;
} }
/**
* Checks whether mPairingController is set
* @return True when mPairingController is set, False otherwise
*/
boolean isPairingControllerSet() {
return mPairingController != null;
}
/**
* Sets the BluetoothPairingDialog activity that started this fragment
* @param pairingDialogActivity The pairing dialog activty that started this fragment
*/
void setPairingDialogActivity(BluetoothPairingDialog pairingDialogActivity) {
if (isPairingDialogActivitySet()) {
throw new IllegalStateException("The pairing dialog activity can only be set once");
}
mPairingDialogActivity = pairingDialogActivity;
}
/**
* Checks whether mPairingDialogActivity is set
* @return True when mPairingDialogActivity is set, False otherwise
*/
boolean isPairingDialogActivitySet() {
return mPairingDialogActivity != null;
}
/** /**
* Creates the appropriate type of dialog and returns it. * Creates the appropriate type of dialog and returns it.
*/ */

View File

@@ -52,7 +52,7 @@ public final class BluetoothPairingRequest extends BroadcastReceiver {
if (powerManager.isInteractive() && shouldShowDialog) { if (powerManager.isInteractive() && shouldShowDialog) {
// Since the screen is on and the BT-related activity is in the foreground, // Since the screen is on and the BT-related activity is in the foreground,
// just open the dialog // just open the dialog
context.startActivity(pairingIntent); context.startActivityAsUser(pairingIntent, UserHandle.CURRENT);
} else { } else {
// Put up a notification that leads to the dialog // Put up a notification that leads to the dialog
intent.setClass(context, BluetoothPairingService.class); intent.setClass(context, BluetoothPairingService.class);

View File

@@ -54,9 +54,13 @@ public class BluetoothPairingDialogTest {
@Mock @Mock
private BluetoothPairingController controller; private BluetoothPairingController controller;
@Mock
private BluetoothPairingDialog dialogActivity;
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
doNothing().when(dialogActivity).dismiss();
} }
@Test @Test
@@ -214,6 +218,17 @@ public class BluetoothPairingDialogTest {
fail("Setting the controller multiple times should throw an exception."); fail("Setting the controller multiple times should throw an exception.");
} }
@Test(expected = IllegalStateException.class)
public void dialogDoesNotAllowSwappingActivity() {
// instantiate a fragment
BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
frag.setPairingDialogActivity(dialogActivity);
// this should throw an error
frag.setPairingDialogActivity(dialogActivity);
fail("Setting the dialog activity multiple times should throw an exception.");
}
@Test @Test
public void dialogPositiveButtonDisabledWhenUserInputInvalid() { public void dialogPositiveButtonDisabledWhenUserInputInvalid() {
// set the correct dialog type // set the correct dialog type
@@ -342,11 +357,52 @@ public class BluetoothPairingDialogTest {
.contains(device); .contains(device);
} }
@Test
public void pairingDialogDismissedOnPositiveClick() {
// set the dialog variant to confirmation/consent
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
// we don't care what this does, just that it is called
doNothing().when(controller).onDialogPositiveClick(any());
// build the fragment
BluetoothPairingDialogFragment frag = makeFragment();
// click the button and verify that the controller hook was called
frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE);
verify(controller, times(1)).onDialogPositiveClick(any());
verify(dialogActivity, times(1)).dismiss();
}
@Test
public void pairingDialogDismissedOnNegativeClick() {
// set the dialog variant to confirmation/consent
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
// we don't care what this does, just that it is called
doNothing().when(controller).onDialogNegativeClick(any());
// build the fragment
BluetoothPairingDialogFragment frag = makeFragment();
// click the button and verify that the controller hook was called
frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE);
verify(controller, times(1)).onDialogNegativeClick(any());
verify(dialogActivity, times(1)).dismiss();
}
private BluetoothPairingDialogFragment makeFragment() { private BluetoothPairingDialogFragment makeFragment() {
BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment(); BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
assertThat(frag.isPairingControllerSet()).isFalse();
frag.setPairingController(controller); frag.setPairingController(controller);
assertThat(frag.isPairingDialogActivitySet()).isFalse();
frag.setPairingDialogActivity(dialogActivity);
FragmentTestUtil.startFragment(frag); FragmentTestUtil.startFragment(frag);
assertThat(frag.getmDialog()).isNotNull(); assertThat(frag.getmDialog()).isNotNull();
assertThat(frag.isPairingControllerSet()).isTrue();
assertThat(frag.isPairingDialogActivitySet()).isTrue();
return frag; return frag;
} }
} }