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