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