1/*
2 * Copyright (C) 2014 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.tv.settings.accessories;
18
19import android.view.WindowManager;
20import com.android.tv.settings.R;
21import com.android.tv.settings.dialog.old.Action;
22import com.android.tv.settings.dialog.old.ActionAdapter;
23import com.android.tv.settings.dialog.old.ActionFragment;
24import com.android.tv.settings.dialog.old.DialogActivity;
25
26import android.app.Fragment;
27import android.bluetooth.BluetoothDevice;
28import android.content.BroadcastReceiver;
29import android.content.Context;
30import android.content.Intent;
31import android.content.IntentFilter;
32import android.graphics.drawable.ColorDrawable;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.Message;
36import android.text.Html;
37import android.text.InputFilter;
38import android.text.InputType;
39import android.text.InputFilter.LengthFilter;
40import android.util.Log;
41import android.view.KeyEvent;
42import android.view.LayoutInflater;
43import android.view.View;
44import android.view.ViewGroup;
45import android.widget.EditText;
46import android.widget.RelativeLayout;
47import android.widget.TextView;
48
49import com.android.tv.settings.util.AccessibilityHelper;
50
51import java.util.ArrayList;
52import java.util.Locale;
53
54/**
55 * BluetoothPairingDialog asks the user to enter a PIN / Passkey / simple
56 * confirmation for pairing with a remote Bluetooth device.
57 */
58public class BluetoothPairingDialog extends DialogActivity {
59
60    private static final String KEY_PAIR = "action_pair";
61    private static final String KEY_CANCEL = "action_cancel";
62
63    private static final String TAG = "aah.BluetoothPairingDialog";
64    private static final boolean DEBUG = false;
65
66    private static final int BLUETOOTH_PIN_MAX_LENGTH = 16;
67    private static final int BLUETOOTH_PASSKEY_MAX_LENGTH = 6;
68
69    private BluetoothDevice mDevice;
70    private int mType;
71    private String mPairingKey;
72
73    private ActionFragment mActionFragment;
74    private Fragment mContentFragment;
75    private ArrayList<Action> mActions;
76
77    private RelativeLayout mTopLayout;
78    protected ColorDrawable mBgDrawable = new ColorDrawable();
79    private TextView mTitleText;
80    private TextView mInstructionText;
81    private EditText mTextInput;
82
83
84    /**
85     * Dismiss the dialog if the bond state changes to bonded or none, or if
86     * pairing was canceled for {@link #mDevice}.
87     */
88    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
89        @Override
90        public void onReceive(Context context, Intent intent) {
91            String action = intent.getAction();
92            if (DEBUG) {
93                Log.d(TAG, "onReceive. Broadcast Intent = " + intent.toString());
94            }
95            if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
96                int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
97                        BluetoothDevice.ERROR);
98                if (bondState == BluetoothDevice.BOND_BONDED ||
99                        bondState == BluetoothDevice.BOND_NONE) {
100                    dismiss();
101                }
102            } else if (BluetoothDevice.ACTION_PAIRING_CANCEL.equals(action)) {
103                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
104                if (device == null || device.equals(mDevice)) {
105                    dismiss();
106                }
107            }
108        }
109    };
110
111    @Override
112    protected void onCreate(Bundle savedInstanceState) {
113        super.onCreate(savedInstanceState);
114
115        Intent intent = getIntent();
116        if (!BluetoothDevice.ACTION_PAIRING_REQUEST.equals(intent.getAction())) {
117            Log.e(TAG, "Error: this activity may be started only with intent " +
118                    BluetoothDevice.ACTION_PAIRING_REQUEST);
119            finish();
120            return;
121        }
122
123        mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
124        mType = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR);
125
126        if (DEBUG) {
127            Log.d(TAG, "Requested pairing Type = " + mType + " , Device = " + mDevice);
128        }
129
130        mActions = new ArrayList<Action>();
131
132        switch (mType) {
133            case BluetoothDevice.PAIRING_VARIANT_PIN:
134            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
135                createUserEntryDialog();
136                break;
137
138            case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
139                int passkey =
140                    intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR);
141                if (passkey == BluetoothDevice.ERROR) {
142                    Log.e(TAG, "Invalid Confirmation Passkey received, not showing any dialog");
143                    finish();
144                    return;
145                }
146                mPairingKey = String.format(Locale.US, "%06d", passkey);
147                createConfirmationDialog();
148                break;
149
150            case BluetoothDevice.PAIRING_VARIANT_CONSENT:
151            case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
152                createConfirmationDialog();
153                break;
154
155            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
156            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
157                int pairingKey =
158                    intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR);
159                if (pairingKey == BluetoothDevice.ERROR) {
160                    Log.e(TAG,
161                            "Invalid Confirmation Passkey or PIN received, not showing any dialog");
162                    finish();
163                    return;
164                }
165                if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) {
166                    mPairingKey = String.format("%06d", pairingKey);
167                } else {
168                    mPairingKey = String.format("%04d", pairingKey);
169                }
170                createConfirmationDialog();
171                break;
172
173            default:
174                Log.e(TAG, "Incorrect pairing type received, not showing any dialog");
175                finish();
176                return;
177        }
178
179        ViewGroup contentView = (ViewGroup) findViewById(android.R.id.content);
180        mTopLayout = (RelativeLayout) contentView.getChildAt(0);
181
182        // Fade out the old activity, and fade in the new activity.
183        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
184
185        // Set the activity background
186        int bgColor = getResources().getColor(R.color.dialog_activity_background);
187        mBgDrawable.setColor(bgColor);
188        mBgDrawable.setAlpha(255);
189        mTopLayout.setBackground(mBgDrawable);
190
191        // Make sure pairing wakes up day dream
192        getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
193                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
194                WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
195                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
196    }
197
198    @Override
199    protected void onResume() {
200        super.onResume();
201
202        IntentFilter filter = new IntentFilter();
203        filter.addAction(BluetoothDevice.ACTION_PAIRING_CANCEL);
204        filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
205        registerReceiver(mReceiver, filter);
206    }
207
208    @Override
209    protected void onPause() {
210        unregisterReceiver(mReceiver);
211
212        // Finish the activity if we get placed in the background and cancel pairing
213        cancelPairing();
214        dismiss();
215
216        super.onPause();
217    }
218
219    @Override
220    public void onActionClicked(Action action) {
221        String key = action.getKey();
222        if (KEY_PAIR.equals(key)) {
223            onPair(null);
224            dismiss();
225        } else if (KEY_CANCEL.equals(key)) {
226            cancelPairing();
227        }
228    }
229
230    @Override
231    public boolean onKeyDown(int keyCode, KeyEvent event) {
232        if (keyCode == KeyEvent.KEYCODE_BACK) {
233            cancelPairing();
234        }
235        return super.onKeyDown(keyCode, event);
236    }
237
238    private ArrayList<Action> getActions() {
239        ArrayList<Action> actions = new ArrayList<Action>();
240
241        switch (mType) {
242            case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
243            case BluetoothDevice.PAIRING_VARIANT_CONSENT:
244            case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
245                actions.add(new Action.Builder()
246                        .key(KEY_PAIR)
247                        .title(getString(R.string.bluetooth_pair))
248                        .build());
249
250                actions.add(new Action.Builder()
251                        .key(KEY_CANCEL)
252                        .title(getString(R.string.bluetooth_cancel))
253                        .build());
254                break;
255            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
256            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
257                actions.add(new Action.Builder()
258                        .key(KEY_CANCEL)
259                        .title(getString(R.string.bluetooth_cancel))
260                        .build());
261                break;
262        }
263
264        return actions;
265    }
266
267    private void dismiss() {
268        finish();
269    }
270
271    private void cancelPairing() {
272        if (DEBUG) {
273            Log.d(TAG, "cancelPairing");
274        }
275        mDevice.cancelPairingUserInput();
276    }
277
278    private void createUserEntryDialog() {
279        setContentView(R.layout.bt_pairing_passkey_entry);
280
281        mTitleText = (TextView) findViewById(R.id.title_text);
282        mTextInput = (EditText) findViewById(R.id.text_input);
283
284        String instructions = getString(R.string.bluetooth_confirm_passkey_msg,
285                mDevice.getName(), mPairingKey);
286        int maxLength;
287        switch (mType) {
288            case BluetoothDevice.PAIRING_VARIANT_PIN:
289                instructions = getString(R.string.bluetooth_enter_pin_msg, mDevice.getName());
290                mInstructionText = (TextView) findViewById(R.id.hint_text);
291                mInstructionText.setText(getString(R.string.bluetooth_pin_values_hint));
292                // Maximum of 16 characters in a PIN
293                maxLength = BLUETOOTH_PIN_MAX_LENGTH;
294                mTextInput.setInputType(InputType.TYPE_CLASS_NUMBER);
295                break;
296
297            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
298                instructions = getString(R.string.bluetooth_enter_passkey_msg, mDevice.getName());
299                // Maximum of 6 digits for passkey
300                maxLength = BLUETOOTH_PASSKEY_MAX_LENGTH;
301                mTextInput.setInputType(InputType.TYPE_CLASS_TEXT);
302                break;
303
304            default:
305                Log.e(TAG, "Incorrect pairing type for createPinEntryView: " + mType);
306                dismiss();
307                return;
308        }
309
310        mTitleText.setText(Html.fromHtml(instructions));
311
312        mTextInput.setFilters(new InputFilter[] { new LengthFilter(maxLength) });
313    }
314
315    private void createConfirmationDialog() {
316        // Build a Dialog activity view, with Action Fragment
317
318        mActions = getActions();
319
320        mActionFragment = ActionFragment.newInstance(mActions);
321        mContentFragment = new Fragment() {
322            @Override
323            public View onCreateView(LayoutInflater inflater, ViewGroup container,
324                    Bundle savedInstanceState) {
325                View v = inflater.inflate(R.layout.bt_pairing_passkey_display, container, false);
326
327                mTitleText = (TextView) v.findViewById(R.id.title);
328                mInstructionText = (TextView) v.findViewById(R.id.pairing_instructions);
329
330                mTitleText.setText(getString(R.string.bluetooth_pairing_request));
331
332                if (AccessibilityHelper.forceFocusableViews(getActivity())) {
333                    mTitleText.setFocusable(true);
334                    mTitleText.setFocusableInTouchMode(true);
335                    mInstructionText.setFocusable(true);
336                    mInstructionText.setFocusableInTouchMode(true);
337                }
338
339                String instructions;
340
341                switch (mType) {
342                    case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
343                    case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
344                        instructions = getString(R.string.bluetooth_display_passkey_pin_msg,
345                                mDevice.getName(), mPairingKey);
346
347                        // Since its only a notification, send an OK to the framework,
348                        // indicating that the dialog has been displayed.
349                        if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) {
350                            mDevice.setPairingConfirmation(true);
351                        } else if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) {
352                            byte[] pinBytes = BluetoothDevice.convertPinToBytes(mPairingKey);
353                            mDevice.setPin(pinBytes);
354                        }
355                        break;
356
357                    case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
358                        instructions = getString(R.string.bluetooth_confirm_passkey_msg,
359                                mDevice.getName(), mPairingKey);
360                        break;
361
362                    case BluetoothDevice.PAIRING_VARIANT_CONSENT:
363                    case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
364                        instructions = getString(R.string.bluetooth_incoming_pairing_msg,
365                                mDevice.getName());
366
367                        break;
368                    default:
369                        instructions = new String();
370                }
371
372                mInstructionText.setText(Html.fromHtml(instructions));
373
374                return v;
375            }
376        };
377
378        setContentAndActionFragments(mContentFragment, mActionFragment);
379    }
380
381    private void onPair(String value) {
382        if (DEBUG) {
383            Log.d(TAG, "onPair: " + value);
384        }
385        switch (mType) {
386            case BluetoothDevice.PAIRING_VARIANT_PIN:
387                byte[] pinBytes = BluetoothDevice.convertPinToBytes(value);
388                if (pinBytes == null) {
389                    return;
390                }
391                mDevice.setPin(pinBytes);
392                break;
393
394            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
395                int passkey = Integer.parseInt(value);
396                mDevice.setPasskey(passkey);
397                break;
398
399            case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
400            case BluetoothDevice.PAIRING_VARIANT_CONSENT:
401                mDevice.setPairingConfirmation(true);
402                break;
403
404            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
405            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
406                // Do nothing.
407                break;
408
409            case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
410                mDevice.setRemoteOutOfBandData();
411                break;
412
413            default:
414                Log.e(TAG, "Incorrect pairing type received");
415        }
416    }
417
418}
419