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