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