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