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