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 android.app.Dialog; 19 import android.app.settings.SettingsEnums; 20 import android.content.Context; 21 import android.content.DialogInterface; 22 import android.content.DialogInterface.OnClickListener; 23 import android.os.Bundle; 24 import android.text.Editable; 25 import android.text.InputFilter; 26 import android.text.InputFilter.LengthFilter; 27 import android.text.InputType; 28 import android.text.TextUtils; 29 import android.text.TextWatcher; 30 import android.util.Log; 31 import android.view.View; 32 import android.view.inputmethod.InputMethodManager; 33 import android.widget.Button; 34 import android.widget.CheckBox; 35 import android.widget.CompoundButton; 36 import android.widget.EditText; 37 import android.widget.TextView; 38 39 import androidx.annotation.VisibleForTesting; 40 import androidx.appcompat.app.AlertDialog; 41 42 import com.android.settings.R; 43 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 44 45 /** 46 * A dialogFragment used by {@link BluetoothPairingDialog} to create an appropriately styled dialog 47 * for the bluetooth device. 48 */ 49 public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment implements 50 TextWatcher, OnClickListener { 51 52 private static final String TAG = "BTPairingDialogFragment"; 53 54 private AlertDialog.Builder mBuilder; 55 private AlertDialog mDialog; 56 private BluetoothPairingController mPairingController; 57 private BluetoothPairingDialog mPairingDialogActivity; 58 private EditText mPairingView; 59 private boolean mPositiveClicked = false; 60 /** 61 * The interface we expect a listener to implement. Typically this should be done by 62 * the controller. 63 */ 64 public interface BluetoothPairingDialogListener { 65 onDialogNegativeClick(BluetoothPairingDialogFragment dialog)66 void onDialogNegativeClick(BluetoothPairingDialogFragment dialog); 67 onDialogPositiveClick(BluetoothPairingDialogFragment dialog)68 void onDialogPositiveClick(BluetoothPairingDialogFragment dialog); 69 } 70 71 @Override onCreateDialog(Bundle savedInstanceState)72 public Dialog onCreateDialog(Bundle savedInstanceState) { 73 if (!isPairingControllerSet()) { 74 throw new IllegalStateException( 75 "Must call setPairingController() before showing dialog"); 76 } 77 if (!isPairingDialogActivitySet()) { 78 throw new IllegalStateException( 79 "Must call setPairingDialogActivity() before showing dialog"); 80 } 81 mBuilder = new AlertDialog.Builder(getActivity()); 82 mDialog = setupDialog(); 83 mDialog.setCanceledOnTouchOutside(false); 84 return mDialog; 85 } 86 87 @Override onDestroy()88 public void onDestroy() { 89 super.onDestroy(); 90 if (mPairingController.getDialogType() 91 != BluetoothPairingController.DISPLAY_PASSKEY_DIALOG) { 92 /* Cancel pairing unless explicitly accepted by user */ 93 if (!mPositiveClicked) { 94 mPairingController.onCancel(); 95 } 96 } 97 } 98 99 @Override beforeTextChanged(CharSequence s, int start, int count, int after)100 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 101 } 102 103 @Override onTextChanged(CharSequence s, int start, int before, int count)104 public void onTextChanged(CharSequence s, int start, int before, int count) { 105 } 106 107 @Override afterTextChanged(Editable s)108 public void afterTextChanged(Editable s) { 109 // enable the positive button when we detect potentially valid input 110 Button positiveButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); 111 if (positiveButton != null) { 112 positiveButton.setEnabled(mPairingController.isPasskeyValid(s)); 113 } 114 // notify the controller about user input 115 mPairingController.updateUserInput(s.toString()); 116 } 117 118 @Override onClick(DialogInterface dialog, int which)119 public void onClick(DialogInterface dialog, int which) { 120 if (which == DialogInterface.BUTTON_POSITIVE) { 121 mPositiveClicked = true; 122 mPairingController.onDialogPositiveClick(this); 123 } else if (which == DialogInterface.BUTTON_NEGATIVE) { 124 mPairingController.onDialogNegativeClick(this); 125 } 126 mPairingDialogActivity.dismiss(); 127 } 128 129 @Override getMetricsCategory()130 public int getMetricsCategory() { 131 return SettingsEnums.BLUETOOTH_DIALOG_FRAGMENT; 132 } 133 134 /** 135 * Used in testing to get a reference to the dialog. 136 * @return - The fragments current dialog 137 */ getmDialog()138 protected AlertDialog getmDialog() { 139 return mDialog; 140 } 141 142 /** 143 * Sets the controller that the fragment should use. this method MUST be called 144 * before you try to show the dialog or an error will be thrown. An implementation 145 * of a pairing controller can be found at {@link BluetoothPairingController}. A 146 * controller may not be substituted once it is assigned. Forcibly switching a 147 * controller for a new one will lead to undefined behavior. 148 */ setPairingController(BluetoothPairingController pairingController)149 void setPairingController(BluetoothPairingController pairingController) { 150 if (isPairingControllerSet()) { 151 throw new IllegalStateException("The controller can only be set once. " 152 + "Forcibly replacing it will lead to undefined behavior"); 153 } 154 mPairingController = pairingController; 155 } 156 157 /** 158 * Checks whether mPairingController is set 159 * @return True when mPairingController is set, False otherwise 160 */ isPairingControllerSet()161 boolean isPairingControllerSet() { 162 return mPairingController != null; 163 } 164 165 /** 166 * Sets the BluetoothPairingDialog activity that started this fragment 167 * @param pairingDialogActivity The pairing dialog activty that started this fragment 168 */ setPairingDialogActivity(BluetoothPairingDialog pairingDialogActivity)169 void setPairingDialogActivity(BluetoothPairingDialog pairingDialogActivity) { 170 if (isPairingDialogActivitySet()) { 171 throw new IllegalStateException("The pairing dialog activity can only be set once"); 172 } 173 mPairingDialogActivity = pairingDialogActivity; 174 } 175 176 /** 177 * Checks whether mPairingDialogActivity is set 178 * @return True when mPairingDialogActivity is set, False otherwise 179 */ isPairingDialogActivitySet()180 boolean isPairingDialogActivitySet() { 181 return mPairingDialogActivity != null; 182 } 183 184 /** 185 * Creates the appropriate type of dialog and returns it. 186 */ setupDialog()187 private AlertDialog setupDialog() { 188 AlertDialog dialog; 189 switch (mPairingController.getDialogType()) { 190 case BluetoothPairingController.USER_ENTRY_DIALOG: 191 dialog = createUserEntryDialog(); 192 break; 193 case BluetoothPairingController.CONFIRMATION_DIALOG: 194 dialog = createConsentDialog(); 195 break; 196 case BluetoothPairingController.DISPLAY_PASSKEY_DIALOG: 197 dialog = createDisplayPasskeyOrPinDialog(); 198 break; 199 default: 200 dialog = null; 201 Log.e(TAG, "Incorrect pairing type received, not showing any dialog"); 202 } 203 return dialog; 204 } 205 206 /** 207 * Helper method to return the text of the pin entry field - this exists primarily to help us 208 * simulate having existing text when the dialog is recreated, for example after a screen 209 * rotation. 210 */ 211 @VisibleForTesting getPairingViewText()212 CharSequence getPairingViewText() { 213 if (mPairingView != null) { 214 return mPairingView.getText(); 215 } 216 return null; 217 } 218 219 /** 220 * Returns a dialog with UI elements that allow a user to provide input. 221 */ createUserEntryDialog()222 private AlertDialog createUserEntryDialog() { 223 mBuilder.setTitle(getString(R.string.bluetooth_pairing_request, 224 mPairingController.getDeviceName())); 225 mBuilder.setView(createPinEntryView()); 226 mBuilder.setPositiveButton(getString(android.R.string.ok), this); 227 mBuilder.setNegativeButton(getString(android.R.string.cancel), this); 228 AlertDialog dialog = mBuilder.create(); 229 dialog.setOnShowListener(d -> { 230 if (TextUtils.isEmpty(getPairingViewText())) { 231 mDialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled(false); 232 } 233 if (mPairingView != null && mPairingView.requestFocus()) { 234 InputMethodManager imm = (InputMethodManager) 235 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 236 if (imm != null) { 237 imm.showSoftInput(mPairingView, InputMethodManager.SHOW_IMPLICIT); 238 } 239 } 240 }); 241 return dialog; 242 } 243 244 /** 245 * Creates the custom view with UI elements for user input. 246 */ createPinEntryView()247 private View createPinEntryView() { 248 View view = getActivity().getLayoutInflater().inflate(R.layout.bluetooth_pin_entry, null); 249 TextView messageViewCaptionHint = (TextView) view.findViewById(R.id.pin_values_hint); 250 TextView messageView2 = (TextView) view.findViewById(R.id.message_below_pin); 251 CheckBox alphanumericPin = (CheckBox) view.findViewById(R.id.alphanumeric_pin); 252 CheckBox contactSharing = (CheckBox) view.findViewById( 253 R.id.phonebook_sharing_message_entry_pin); 254 contactSharing.setText(getString(R.string.bluetooth_pairing_shares_phonebook)); 255 EditText pairingView = (EditText) view.findViewById(R.id.text); 256 257 contactSharing.setVisibility( 258 mPairingController.isContactSharingVisible() ? View.VISIBLE : View.GONE); 259 mPairingController.setContactSharingState(); 260 contactSharing.setOnCheckedChangeListener(mPairingController); 261 contactSharing.setChecked(mPairingController.getContactSharingState()); 262 263 mPairingView = pairingView; 264 265 pairingView.setInputType(InputType.TYPE_CLASS_NUMBER); 266 pairingView.addTextChangedListener(this); 267 alphanumericPin.setOnCheckedChangeListener((buttonView, isChecked) -> { 268 // change input type for soft keyboard to numeric or alphanumeric 269 if (isChecked) { 270 mPairingView.setInputType(InputType.TYPE_CLASS_TEXT); 271 } else { 272 mPairingView.setInputType(InputType.TYPE_CLASS_NUMBER); 273 } 274 }); 275 276 int messageId = mPairingController.getDeviceVariantMessageId(); 277 int messageIdHint = mPairingController.getDeviceVariantMessageHintId(); 278 int maxLength = mPairingController.getDeviceMaxPasskeyLength(); 279 alphanumericPin.setVisibility(mPairingController.pairingCodeIsAlphanumeric() 280 ? View.VISIBLE : View.GONE); 281 if (messageId != BluetoothPairingController.INVALID_DIALOG_TYPE) { 282 messageView2.setText(messageId); 283 } else { 284 messageView2.setVisibility(View.GONE); 285 } 286 if (messageIdHint != BluetoothPairingController.INVALID_DIALOG_TYPE) { 287 messageViewCaptionHint.setText(messageIdHint); 288 } else { 289 messageViewCaptionHint.setVisibility(View.GONE); 290 } 291 pairingView.setFilters(new InputFilter[]{ 292 new LengthFilter(maxLength)}); 293 294 return view; 295 } 296 297 /** 298 * Creates a dialog with UI elements that allow the user to confirm a pairing request. 299 */ createConfirmationDialog()300 private AlertDialog createConfirmationDialog() { 301 mBuilder.setTitle(getString(R.string.bluetooth_pairing_request, 302 mPairingController.getDeviceName())); 303 mBuilder.setView(createView()); 304 mBuilder.setPositiveButton( 305 getString(com.android.settingslib.R.string.bluetooth_pairing_accept), this); 306 mBuilder.setNegativeButton( 307 getString(com.android.settingslib.R.string.bluetooth_pairing_decline), this); 308 AlertDialog dialog = mBuilder.create(); 309 return dialog; 310 } 311 312 /** 313 * Creates a dialog with UI elements that allow the user to consent to a pairing request. 314 */ createConsentDialog()315 private AlertDialog createConsentDialog() { 316 return createConfirmationDialog(); 317 } 318 319 /** 320 * Creates a dialog that informs users of a pairing request and shows them the passkey/pin 321 * of the device. 322 */ createDisplayPasskeyOrPinDialog()323 private AlertDialog createDisplayPasskeyOrPinDialog() { 324 mBuilder.setTitle(getString(R.string.bluetooth_pairing_request, 325 mPairingController.getDeviceName())); 326 mBuilder.setView(createView()); 327 mBuilder.setNegativeButton(getString(android.R.string.cancel), this); 328 AlertDialog dialog = mBuilder.create(); 329 330 // Tell the controller the dialog has been created. 331 mPairingController.notifyDialogDisplayed(); 332 333 return dialog; 334 } 335 336 /** 337 * Creates a custom view for dialogs which need to show users additional information but do 338 * not require user input. 339 */ createView()340 private View createView() { 341 View view = getActivity().getLayoutInflater().inflate(R.layout.bluetooth_pin_confirm, null); 342 TextView pairingViewCaption = (TextView) view.findViewById(R.id.pairing_caption); 343 TextView pairingViewContent = (TextView) view.findViewById(R.id.pairing_subhead); 344 TextView messagePairing = (TextView) view.findViewById(R.id.pairing_code_message); 345 CompoundButton contactSharing = 346 view.findViewById(R.id.phonebook_sharing_message_confirm_pin); 347 view.findViewById(R.id.phonebook_sharing).setVisibility( 348 mPairingController.isContactSharingVisible() ? View.VISIBLE : View.GONE); 349 mPairingController.setContactSharingState(); 350 contactSharing.setChecked(mPairingController.getContactSharingState()); 351 contactSharing.setOnCheckedChangeListener(mPairingController); 352 353 messagePairing.setVisibility(mPairingController.isDisplayPairingKeyVariant() 354 ? View.VISIBLE : View.GONE); 355 if (mPairingController.hasPairingContent()) { 356 pairingViewCaption.setVisibility(View.VISIBLE); 357 pairingViewContent.setVisibility(View.VISIBLE); 358 pairingViewContent.setText(mPairingController.getPairingContent()); 359 } 360 final TextView messagePairingSet = (TextView) view.findViewById(R.id.pairing_group_message); 361 if (mPairingController.isLateBonding()) { 362 messagePairingSet.setText(getString(R.string.bluetooth_pairing_group_late_bonding)); 363 } 364 365 boolean setPairingMessage = 366 mPairingController.isCoordinatedSetMemberDevice() || mPairingController.isLateBonding(); 367 368 messagePairingSet.setVisibility(setPairingMessage ? View.VISIBLE : View.GONE); 369 return view; 370 } 371 } 372