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