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