1 /*
2  * Copyright (C) 2008 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 
17 package com.android.settings.bluetooth;
18 
19 import android.bluetooth.BluetoothClass;
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothUuid;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.DialogInterface;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.os.Bundle;
28 import android.text.Editable;
29 import android.text.InputFilter;
30 import android.text.InputFilter.LengthFilter;
31 import android.text.InputType;
32 import android.text.TextWatcher;
33 import android.util.Log;
34 import android.view.KeyEvent;
35 import android.view.View;
36 import android.widget.Button;
37 import android.widget.CheckBox;
38 import android.widget.CompoundButton;
39 import android.widget.EditText;
40 import android.widget.TextView;
41 
42 import com.android.internal.app.AlertActivity;
43 import com.android.internal.app.AlertController;
44 import com.android.settings.R;
45 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
46 import com.android.settingslib.bluetooth.LocalBluetoothManager;
47 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
48 
49 import java.util.Locale;
50 
51 /**
52  * BluetoothPairingDialog asks the user to enter a PIN / Passkey / simple confirmation
53  * for pairing with a remote Bluetooth device. It is an activity that appears as a dialog.
54  */
55 public final class BluetoothPairingDialog extends AlertActivity implements
56         CompoundButton.OnCheckedChangeListener, DialogInterface.OnClickListener, TextWatcher {
57     private static final String TAG = "BluetoothPairingDialog";
58 
59     private static final int BLUETOOTH_PIN_MAX_LENGTH = 16;
60     private static final int BLUETOOTH_PASSKEY_MAX_LENGTH = 6;
61 
62     private LocalBluetoothManager mBluetoothManager;
63     private CachedBluetoothDeviceManager mCachedDeviceManager;
64     private BluetoothDevice mDevice;
65     private int mType;
66     private String mPairingKey;
67     private EditText mPairingView;
68     private Button mOkButton;
69     private LocalBluetoothProfile mPbapClientProfile;
70 
71 
72     /**
73      * Dismiss the dialog if the bond state changes to bonded or none,
74      * or if pairing was canceled for {@link #mDevice}.
75      */
76     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
77         @Override
78         public void onReceive(Context context, Intent intent) {
79             String action = intent.getAction();
80             if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
81                 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
82                                                    BluetoothDevice.ERROR);
83                 if (bondState == BluetoothDevice.BOND_BONDED ||
84                         bondState == BluetoothDevice.BOND_NONE) {
85                     dismiss();
86                 }
87             } else if (BluetoothDevice.ACTION_PAIRING_CANCEL.equals(action)) {
88                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
89                 if (device == null || device.equals(mDevice)) {
90                     dismiss();
91                 }
92             }
93         }
94     };
95 
96     @Override
onCreate(Bundle savedInstanceState)97     protected void onCreate(Bundle savedInstanceState) {
98         super.onCreate(savedInstanceState);
99 
100         Intent intent = getIntent();
101         if (!intent.getAction().equals(BluetoothDevice.ACTION_PAIRING_REQUEST))
102         {
103             Log.e(TAG, "Error: this activity may be started only with intent " +
104                   BluetoothDevice.ACTION_PAIRING_REQUEST);
105             finish();
106             return;
107         }
108 
109         mBluetoothManager = Utils.getLocalBtManager(this);
110         if (mBluetoothManager == null) {
111             Log.e(TAG, "Error: BluetoothAdapter not supported by system");
112             finish();
113             return;
114         }
115         mCachedDeviceManager = mBluetoothManager.getCachedDeviceManager();
116         mPbapClientProfile = mBluetoothManager.getProfileManager().getPbapClientProfile();
117 
118         mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
119         mType = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR);
120 
121         switch (mType) {
122             case BluetoothDevice.PAIRING_VARIANT_PIN:
123             case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
124             case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
125                 createUserEntryDialog();
126                 break;
127 
128             case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
129                 int passkey =
130                     intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR);
131                 if (passkey == BluetoothDevice.ERROR) {
132                     Log.e(TAG, "Invalid Confirmation Passkey received, not showing any dialog");
133                     return;
134                 }
135                 mPairingKey = String.format(Locale.US, "%06d", passkey);
136                 createConfirmationDialog();
137                 break;
138 
139             case BluetoothDevice.PAIRING_VARIANT_CONSENT:
140             case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
141                 createConsentDialog();
142                 break;
143 
144             case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
145             case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
146                 int pairingKey =
147                     intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR);
148                 if (pairingKey == BluetoothDevice.ERROR) {
149                     Log.e(TAG, "Invalid Confirmation Passkey or PIN received, not showing any dialog");
150                     return;
151                 }
152                 if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) {
153                     mPairingKey = String.format("%06d", pairingKey);
154                 } else {
155                     mPairingKey = String.format("%04d", pairingKey);
156                 }
157                 createDisplayPasskeyOrPinDialog();
158                 break;
159 
160             default:
161                 Log.e(TAG, "Incorrect pairing type received, not showing any dialog");
162         }
163 
164         /*
165          * Leave this registered through pause/resume since we still want to
166          * finish the activity in the background if pairing is canceled.
167          */
168         registerReceiver(mReceiver, new IntentFilter(BluetoothDevice.ACTION_PAIRING_CANCEL));
169         registerReceiver(mReceiver, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
170     }
171 
createUserEntryDialog()172     private void createUserEntryDialog() {
173         final AlertController.AlertParams p = mAlertParams;
174         p.mTitle = getString(R.string.bluetooth_pairing_request,
175                 mCachedDeviceManager.getName(mDevice));
176         p.mView = createPinEntryView();
177         p.mPositiveButtonText = getString(android.R.string.ok);
178         p.mPositiveButtonListener = this;
179         p.mNegativeButtonText = getString(android.R.string.cancel);
180         p.mNegativeButtonListener = this;
181         setupAlert();
182 
183         mOkButton = mAlert.getButton(BUTTON_POSITIVE);
184         mOkButton.setEnabled(false);
185     }
186 
createPinEntryView()187     private View createPinEntryView() {
188         View view = getLayoutInflater().inflate(R.layout.bluetooth_pin_entry, null);
189         TextView messageViewCaptionHint = (TextView) view.findViewById(R.id.pin_values_hint);
190         TextView messageView2 = (TextView) view.findViewById(R.id.message_below_pin);
191         CheckBox alphanumericPin = (CheckBox) view.findViewById(R.id.alphanumeric_pin);
192         CheckBox contactSharing = (CheckBox) view.findViewById(
193                 R.id.phonebook_sharing_message_entry_pin);
194         contactSharing.setText(getString(R.string.bluetooth_pairing_shares_phonebook,
195                 mCachedDeviceManager.getName(mDevice)));
196         if (mPbapClientProfile != null && mPbapClientProfile.isProfileReady()) {
197             contactSharing.setVisibility(View.GONE);
198         }
199         if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_ALLOWED) {
200             contactSharing.setChecked(true);
201         } else if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_REJECTED){
202             contactSharing.setChecked(false);
203         } else {
204             if (mDevice.getBluetoothClass().getDeviceClass()
205                     == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE) {
206                 contactSharing.setChecked(true);
207                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
208             } else {
209                 contactSharing.setChecked(false);
210                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
211             }
212         }
213 
214         contactSharing.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
215             @Override
216             public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
217                 if (isChecked) {
218                     mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
219                 } else {
220                     mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
221                 }
222             }
223         });
224 
225         mPairingView = (EditText) view.findViewById(R.id.text);
226         mPairingView.addTextChangedListener(this);
227         alphanumericPin.setOnCheckedChangeListener(this);
228 
229         int messageId;
230         int messageIdHint = R.string.bluetooth_pin_values_hint;
231         int maxLength;
232         switch (mType) {
233             case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
234                 messageIdHint = R.string.bluetooth_pin_values_hint_16_digits;
235                 // FALLTHROUGH
236             case BluetoothDevice.PAIRING_VARIANT_PIN:
237                 messageId = R.string.bluetooth_enter_pin_other_device;
238                 // Maximum of 16 characters in a PIN
239                 maxLength = BLUETOOTH_PIN_MAX_LENGTH;
240                 break;
241 
242             case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
243                 messageId = R.string.bluetooth_enter_passkey_other_device;
244                 // Maximum of 6 digits for passkey
245                 maxLength = BLUETOOTH_PASSKEY_MAX_LENGTH;
246                 alphanumericPin.setVisibility(View.GONE);
247                 break;
248 
249             default:
250                 Log.e(TAG, "Incorrect pairing type for createPinEntryView: " + mType);
251                 return null;
252         }
253 
254         messageViewCaptionHint.setText(messageIdHint);
255         messageView2.setText(messageId);
256         mPairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
257         mPairingView.setFilters(new InputFilter[] {
258                 new LengthFilter(maxLength) });
259 
260         return view;
261     }
262 
createView()263     private View createView() {
264         View view = getLayoutInflater().inflate(R.layout.bluetooth_pin_confirm, null);
265         TextView pairingViewCaption = (TextView) view.findViewById(R.id.pairing_caption);
266         TextView pairingViewContent = (TextView) view.findViewById(R.id.pairing_subhead);
267         TextView messagePairing = (TextView) view.findViewById(R.id.pairing_code_message);
268         CheckBox contactSharing = (CheckBox) view.findViewById(
269                 R.id.phonebook_sharing_message_confirm_pin);
270         contactSharing.setText(getString(R.string.bluetooth_pairing_shares_phonebook,
271                 mCachedDeviceManager.getName(mDevice)));
272         if (mPbapClientProfile != null && mPbapClientProfile.isProfileReady()) {
273             contactSharing.setVisibility(View.GONE);
274         }
275         if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_ALLOWED) {
276             contactSharing.setChecked(true);
277         } else if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_REJECTED){
278             contactSharing.setChecked(false);
279         } else {
280             if (mDevice.getBluetoothClass().getDeviceClass()
281                     == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE) {
282                 contactSharing.setChecked(true);
283                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
284             } else {
285                 contactSharing.setChecked(false);
286                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
287             }
288         }
289 
290         contactSharing.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
291             @Override
292             public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
293                 if (isChecked) {
294                     mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
295                 } else {
296                     mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
297                 }
298             }
299         });
300 
301         String messageCaption = null;
302         String pairingContent = null;
303         switch (mType) {
304             case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
305             case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
306                 messagePairing.setVisibility(View.VISIBLE);
307             case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
308                 pairingContent = mPairingKey;
309                 break;
310 
311             case BluetoothDevice.PAIRING_VARIANT_CONSENT:
312             case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
313                 messagePairing.setVisibility(View.VISIBLE);
314                 break;
315 
316             default:
317                 Log.e(TAG, "Incorrect pairing type received, not creating view");
318                 return null;
319         }
320 
321         if (pairingContent != null) {
322             pairingViewCaption.setVisibility(View.VISIBLE);
323             pairingViewContent.setVisibility(View.VISIBLE);
324             pairingViewContent.setText(pairingContent);
325         }
326 
327         return view;
328     }
329 
createConfirmationDialog()330     private void createConfirmationDialog() {
331         final AlertController.AlertParams p = mAlertParams;
332         p.mTitle = getString(R.string.bluetooth_pairing_request,
333                 mCachedDeviceManager.getName(mDevice));
334         p.mView = createView();
335         p.mPositiveButtonText = getString(R.string.bluetooth_pairing_accept);
336         p.mPositiveButtonListener = this;
337         p.mNegativeButtonText = getString(R.string.bluetooth_pairing_decline);
338         p.mNegativeButtonListener = this;
339         setupAlert();
340     }
341 
createConsentDialog()342     private void createConsentDialog() {
343         final AlertController.AlertParams p = mAlertParams;
344         p.mTitle = getString(R.string.bluetooth_pairing_request,
345                 mCachedDeviceManager.getName(mDevice));
346         p.mView = createView();
347         p.mPositiveButtonText = getString(R.string.bluetooth_pairing_accept);
348         p.mPositiveButtonListener = this;
349         p.mNegativeButtonText = getString(R.string.bluetooth_pairing_decline);
350         p.mNegativeButtonListener = this;
351         setupAlert();
352     }
353 
createDisplayPasskeyOrPinDialog()354     private void createDisplayPasskeyOrPinDialog() {
355         final AlertController.AlertParams p = mAlertParams;
356         p.mTitle = getString(R.string.bluetooth_pairing_request,
357                 mCachedDeviceManager.getName(mDevice));
358         p.mView = createView();
359         p.mNegativeButtonText = getString(android.R.string.cancel);
360         p.mNegativeButtonListener = this;
361         setupAlert();
362 
363         // Since its only a notification, send an OK to the framework,
364         // indicating that the dialog has been displayed.
365         if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) {
366             mDevice.setPairingConfirmation(true);
367         } else if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) {
368             byte[] pinBytes = BluetoothDevice.convertPinToBytes(mPairingKey);
369             mDevice.setPin(pinBytes);
370         }
371     }
372 
373     @Override
onDestroy()374     protected void onDestroy() {
375         super.onDestroy();
376         unregisterReceiver(mReceiver);
377     }
378 
afterTextChanged(Editable s)379     public void afterTextChanged(Editable s) {
380         if (mOkButton != null) {
381             if (mType == BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS) {
382                 mOkButton.setEnabled(s.length() >= 16);
383             } else {
384                 mOkButton.setEnabled(s.length() > 0);
385             }
386         }
387     }
388 
onPair(String value)389     private void onPair(String value) {
390         switch (mType) {
391             case BluetoothDevice.PAIRING_VARIANT_PIN:
392             case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
393                 byte[] pinBytes = BluetoothDevice.convertPinToBytes(value);
394                 if (pinBytes == null) {
395                     return;
396                 }
397                 mDevice.setPin(pinBytes);
398                 break;
399 
400             case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
401                 int passkey = Integer.parseInt(value);
402                 mDevice.setPasskey(passkey);
403                 break;
404 
405             case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
406             case BluetoothDevice.PAIRING_VARIANT_CONSENT:
407                 mDevice.setPairingConfirmation(true);
408                 break;
409 
410             case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
411             case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
412                 // Do nothing.
413                 break;
414 
415             case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
416                 mDevice.setRemoteOutOfBandData();
417                 break;
418 
419             default:
420                 Log.e(TAG, "Incorrect pairing type received");
421         }
422     }
423 
onCancel()424     private void onCancel() {
425         mDevice.cancelPairingUserInput();
426     }
427 
onKeyDown(int keyCode, KeyEvent event)428     public boolean onKeyDown(int keyCode, KeyEvent event) {
429         if (keyCode == KeyEvent.KEYCODE_BACK) {
430             onCancel();
431         }
432         return super.onKeyDown(keyCode,event);
433     }
434 
onClick(DialogInterface dialog, int which)435     public void onClick(DialogInterface dialog, int which) {
436         switch (which) {
437             case BUTTON_POSITIVE:
438                 if (mPairingView != null) {
439                     onPair(mPairingView.getText().toString());
440                 } else {
441                     onPair(null);
442                 }
443                 break;
444 
445             case BUTTON_NEGATIVE:
446             default:
447                 onCancel();
448                 break;
449         }
450     }
451 
452     /* Not used */
beforeTextChanged(CharSequence s, int start, int count, int after)453     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
454     }
455 
456     /* Not used */
onTextChanged(CharSequence s, int start, int before, int count)457     public void onTextChanged(CharSequence s, int start, int before, int count) {
458     }
459 
onCheckedChanged(CompoundButton buttonView, boolean isChecked)460     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
461         // change input type for soft keyboard to numeric or alphanumeric
462         if (isChecked) {
463             mPairingView.setInputType(InputType.TYPE_CLASS_TEXT);
464         } else {
465             mPairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
466         }
467     }
468 }
469