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