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