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