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