blob: e11c275bded8ada3a514e6ea28873e2a67062bc3 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.bluetooth;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputFilter.LengthFilter;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
/**
* A dialogFragment used by {@link BluetoothPairingDialog} to create an appropriately styled dialog
* for the bluetooth device.
*/
public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment implements
TextWatcher, OnClickListener {
private static final String TAG = "BTPairingDialogFragment";
private AlertDialog.Builder mBuilder;
private AlertDialog mDialog;
private BluetoothPairingController mPairingController;
private BluetoothPairingDialog mPairingDialogActivity;
private EditText mPairingView;
private boolean mPositiveClicked = false;
/**
* The interface we expect a listener to implement. Typically this should be done by
* the controller.
*/
public interface BluetoothPairingDialogListener {
void onDialogNegativeClick(BluetoothPairingDialogFragment dialog);
void onDialogPositiveClick(BluetoothPairingDialogFragment dialog);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
if (!isPairingControllerSet()) {
throw new IllegalStateException(
"Must call setPairingController() before showing dialog");
}
if (!isPairingDialogActivitySet()) {
throw new IllegalStateException(
"Must call setPairingDialogActivity() before showing dialog");
}
mBuilder = new AlertDialog.Builder(getActivity());
mDialog = setupDialog();
mDialog.setCanceledOnTouchOutside(false);
return mDialog;
}
@Override
public void onDestroy() {
super.onDestroy();
if (mPairingController.getDialogType()
!= BluetoothPairingController.DISPLAY_PASSKEY_DIALOG) {
/* Cancel pairing unless explicitly accepted by user */
if (!mPositiveClicked) {
mPairingController.onCancel();
}
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
// enable the positive button when we detect potentially valid input
Button positiveButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
if (positiveButton != null) {
positiveButton.setEnabled(mPairingController.isPasskeyValid(s));
}
// notify the controller about user input
mPairingController.updateUserInput(s.toString());
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
mPositiveClicked = true;
mPairingController.onDialogPositiveClick(this);
} else if (which == DialogInterface.BUTTON_NEGATIVE) {
mPairingController.onDialogNegativeClick(this);
}
mPairingDialogActivity.dismiss();
}
@Override
public int getMetricsCategory() {
return SettingsEnums.BLUETOOTH_DIALOG_FRAGMENT;
}
/**
* Used in testing to get a reference to the dialog.
* @return - The fragments current dialog
*/
protected AlertDialog getmDialog() {
return mDialog;
}
/**
* Sets the controller that the fragment should use. this method MUST be called
* before you try to show the dialog or an error will be thrown. An implementation
* of a pairing controller can be found at {@link BluetoothPairingController}. A
* controller may not be substituted once it is assigned. Forcibly switching a
* controller for a new one will lead to undefined behavior.
*/
void setPairingController(BluetoothPairingController pairingController) {
if (isPairingControllerSet()) {
throw new IllegalStateException("The controller can only be set once. "
+ "Forcibly replacing it will lead to undefined behavior");
}
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.
*/
private AlertDialog setupDialog() {
AlertDialog dialog;
switch (mPairingController.getDialogType()) {
case BluetoothPairingController.USER_ENTRY_DIALOG:
dialog = createUserEntryDialog();
break;
case BluetoothPairingController.CONFIRMATION_DIALOG:
dialog = createConsentDialog();
break;
case BluetoothPairingController.DISPLAY_PASSKEY_DIALOG:
dialog = createDisplayPasskeyOrPinDialog();
break;
default:
dialog = null;
Log.e(TAG, "Incorrect pairing type received, not showing any dialog");
}
return dialog;
}
/**
* Helper method to return the text of the pin entry field - this exists primarily to help us
* simulate having existing text when the dialog is recreated, for example after a screen
* rotation.
*/
@VisibleForTesting
CharSequence getPairingViewText() {
if (mPairingView != null) {
return mPairingView.getText();
}
return null;
}
/**
* Returns a dialog with UI elements that allow a user to provide input.
*/
private AlertDialog createUserEntryDialog() {
mBuilder.setTitle(getString(R.string.bluetooth_pairing_request,
mPairingController.getDeviceName()));
mBuilder.setView(createPinEntryView());
mBuilder.setPositiveButton(getString(android.R.string.ok), this);
mBuilder.setNegativeButton(getString(android.R.string.cancel), this);
AlertDialog dialog = mBuilder.create();
dialog.setOnShowListener(d -> {
if (TextUtils.isEmpty(getPairingViewText())) {
mDialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled(false);
}
if (mPairingView != null && mPairingView.requestFocus()) {
InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(mPairingView, InputMethodManager.SHOW_IMPLICIT);
}
}
});
return dialog;
}
/**
* Creates the custom view with UI elements for user input.
*/
private View createPinEntryView() {
View view = getActivity().getLayoutInflater().inflate(R.layout.bluetooth_pin_entry, null);
TextView messageViewCaptionHint = (TextView) view.findViewById(R.id.pin_values_hint);
TextView messageView2 = (TextView) view.findViewById(R.id.message_below_pin);
CheckBox alphanumericPin = (CheckBox) view.findViewById(R.id.alphanumeric_pin);
CheckBox contactSharing = (CheckBox) view.findViewById(
R.id.phonebook_sharing_message_entry_pin);
contactSharing.setText(getString(R.string.bluetooth_pairing_shares_phonebook));
EditText pairingView = (EditText) view.findViewById(R.id.text);
contactSharing.setVisibility(
mPairingController.isContactSharingVisible() ? View.VISIBLE : View.GONE);
mPairingController.setContactSharingState();
contactSharing.setOnCheckedChangeListener(mPairingController);
contactSharing.setChecked(mPairingController.getContactSharingState());
mPairingView = pairingView;
pairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
pairingView.addTextChangedListener(this);
alphanumericPin.setOnCheckedChangeListener((buttonView, isChecked) -> {
// change input type for soft keyboard to numeric or alphanumeric
if (isChecked) {
mPairingView.setInputType(InputType.TYPE_CLASS_TEXT);
} else {
mPairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
}
});
int messageId = mPairingController.getDeviceVariantMessageId();
int messageIdHint = mPairingController.getDeviceVariantMessageHintId();
int maxLength = mPairingController.getDeviceMaxPasskeyLength();
alphanumericPin.setVisibility(mPairingController.pairingCodeIsAlphanumeric()
? View.VISIBLE : View.GONE);
if (messageId != BluetoothPairingController.INVALID_DIALOG_TYPE) {
messageView2.setText(messageId);
} else {
messageView2.setVisibility(View.GONE);
}
if (messageIdHint != BluetoothPairingController.INVALID_DIALOG_TYPE) {
messageViewCaptionHint.setText(messageIdHint);
} else {
messageViewCaptionHint.setVisibility(View.GONE);
}
pairingView.setFilters(new InputFilter[]{
new LengthFilter(maxLength)});
return view;
}
/**
* Creates a dialog with UI elements that allow the user to confirm a pairing request.
*/
private AlertDialog createConfirmationDialog() {
mBuilder.setTitle(getString(R.string.bluetooth_pairing_request,
mPairingController.getDeviceName()));
mBuilder.setView(createView());
mBuilder.setPositiveButton(getString(R.string.bluetooth_pairing_accept), this);
mBuilder.setNegativeButton(getString(R.string.bluetooth_pairing_decline), this);
AlertDialog dialog = mBuilder.create();
return dialog;
}
/**
* Creates a dialog with UI elements that allow the user to consent to a pairing request.
*/
private AlertDialog createConsentDialog() {
return createConfirmationDialog();
}
/**
* Creates a dialog that informs users of a pairing request and shows them the passkey/pin
* of the device.
*/
private AlertDialog createDisplayPasskeyOrPinDialog() {
mBuilder.setTitle(getString(R.string.bluetooth_pairing_request,
mPairingController.getDeviceName()));
mBuilder.setView(createView());
mBuilder.setNegativeButton(getString(android.R.string.cancel), this);
AlertDialog dialog = mBuilder.create();
// Tell the controller the dialog has been created.
mPairingController.notifyDialogDisplayed();
return dialog;
}
/**
* Creates a custom view for dialogs which need to show users additional information but do
* not require user input.
*/
private View createView() {
View view = getActivity().getLayoutInflater().inflate(R.layout.bluetooth_pin_confirm, null);
TextView pairingViewCaption = (TextView) view.findViewById(R.id.pairing_caption);
TextView pairingViewContent = (TextView) view.findViewById(R.id.pairing_subhead);
TextView messagePairing = (TextView) view.findViewById(R.id.pairing_code_message);
CheckBox contactSharing = (CheckBox) view.findViewById(
R.id.phonebook_sharing_message_confirm_pin);
contactSharing.setText(getString(R.string.bluetooth_pairing_shares_phonebook));
contactSharing.setVisibility(
mPairingController.isContactSharingVisible() ? View.VISIBLE : View.GONE);
mPairingController.setContactSharingState();
contactSharing.setChecked(mPairingController.getContactSharingState());
contactSharing.setOnCheckedChangeListener(mPairingController);
messagePairing.setVisibility(mPairingController.isDisplayPairingKeyVariant()
? View.VISIBLE : View.GONE);
if (mPairingController.hasPairingContent()) {
pairingViewCaption.setVisibility(View.VISIBLE);
pairingViewContent.setVisibility(View.VISIBLE);
pairingViewContent.setText(mPairingController.getPairingContent());
}
final TextView messagePairingSet = (TextView) view.findViewById(R.id.pairing_group_message);
if (mPairingController.isLateBonding()) {
messagePairingSet.setText(getString(R.string.bluetooth_pairing_group_late_bonding));
}
boolean setPairingMessage =
mPairingController.isCoordinatedSetMemberDevice() || mPairingController.isLateBonding();
messagePairingSet.setVisibility(setPairingMessage ? View.VISIBLE : View.GONE);
return view;
}
}