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