blob: 63638e3cef412538f4c7d34dbf7316b4d2528ac3 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.settings.accessories;
import android.app.Fragment;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.Html;
import android.text.InputFilter;
import android.text.InputFilter.LengthFilter;
import android.text.InputType;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import com.android.internal.logging.nano.MetricsProto;
import com.android.tv.settings.R;
import com.android.tv.settings.dialog.old.Action;
import com.android.tv.settings.dialog.old.ActionFragment;
import com.android.tv.settings.dialog.old.DialogActivity;
import com.android.tv.settings.util.AccessibilityHelper;
import java.util.ArrayList;
import java.util.Locale;
/**
* BluetoothPairingDialog asks the user to enter a PIN / Passkey / simple
* confirmation for pairing with a remote Bluetooth device.
*/
public class BluetoothPairingDialog extends DialogActivity {
private static final String KEY_PAIR = "action_pair";
private static final String KEY_CANCEL = "action_cancel";
private static final String TAG = "BluetoothPairingDialog";
private static final boolean DEBUG = false;
private static final int BLUETOOTH_PIN_MAX_LENGTH = 16;
private static final int BLUETOOTH_PASSKEY_MAX_LENGTH = 6;
private BluetoothDevice mDevice;
private int mType;
private String mPairingKey;
private boolean mPairingInProgress = false;
/**
* Dismiss the dialog if the bond state changes to bonded or none, or if
* pairing was canceled for {@link #mDevice}.
*/
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (DEBUG) {
Log.d(TAG, "onReceive. Broadcast Intent = " + intent.toString());
}
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothDevice.ERROR);
if (bondState == BluetoothDevice.BOND_BONDED ||
bondState == BluetoothDevice.BOND_NONE) {
dismiss();
}
} else if (BluetoothDevice.ACTION_PAIRING_CANCEL.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device == null || device.equals(mDevice)) {
dismiss();
}
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = getIntent();
if (!BluetoothDevice.ACTION_PAIRING_REQUEST.equals(intent.getAction())) {
Log.e(TAG, "Error: this activity may be started only with intent " +
BluetoothDevice.ACTION_PAIRING_REQUEST);
finish();
return;
}
mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
mType = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR);
if (DEBUG) {
Log.d(TAG, "Requested pairing Type = " + mType + " , Device = " + mDevice);
}
switch (mType) {
case BluetoothDevice.PAIRING_VARIANT_PIN:
case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
createUserEntryDialog();
break;
case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
int passkey =
intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR);
if (passkey == BluetoothDevice.ERROR) {
Log.e(TAG, "Invalid Confirmation Passkey received, not showing any dialog");
finish();
return;
}
mPairingKey = String.format(Locale.US, "%06d", passkey);
createConfirmationDialog();
break;
case BluetoothDevice.PAIRING_VARIANT_CONSENT:
case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
createConfirmationDialog();
break;
case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
int pairingKey =
intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR);
if (pairingKey == BluetoothDevice.ERROR) {
Log.e(TAG,
"Invalid Confirmation Passkey or PIN received, not showing any dialog");
finish();
return;
}
if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) {
mPairingKey = String.format("%06d", pairingKey);
} else {
mPairingKey = String.format("%04d", pairingKey);
}
createConfirmationDialog();
break;
default:
Log.e(TAG, "Incorrect pairing type received, not showing any dialog");
finish();
return;
}
// Fade out the old activity, and fade in the new activity.
overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
// TODO: don't do this
final ViewGroup contentView = (ViewGroup) findViewById(android.R.id.content);
final View topLayout = contentView.getChildAt(0);
// Set the activity background
final ColorDrawable bgDrawable =
new ColorDrawable(getColor(R.color.dialog_activity_background));
bgDrawable.setAlpha(255);
topLayout.setBackground(bgDrawable);
// Make sure pairing wakes up day dream
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
@Override
protected void onResume() {
super.onResume();
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_PAIRING_CANCEL);
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
registerReceiver(mReceiver, filter);
}
@Override
protected void onPause() {
unregisterReceiver(mReceiver);
// Finish the activity if we get placed in the background and cancel pairing
if (!mPairingInProgress) {
cancelPairing();
}
dismiss();
super.onPause();
}
@Override
public void onActionClicked(Action action) {
String key = action.getKey();
if (KEY_PAIR.equals(key)) {
onPair(null);
dismiss();
} else if (KEY_CANCEL.equals(key)) {
cancelPairing();
}
}
@Override
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
cancelPairing();
}
return super.onKeyDown(keyCode, event);
}
private ArrayList<Action> getActions() {
ArrayList<Action> actions = new ArrayList<>();
switch (mType) {
case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
case BluetoothDevice.PAIRING_VARIANT_CONSENT:
case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
actions.add(new Action.Builder()
.key(KEY_PAIR)
.title(getString(R.string.bluetooth_pair))
.build());
actions.add(new Action.Builder()
.key(KEY_CANCEL)
.title(getString(R.string.bluetooth_cancel))
.build());
break;
case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
actions.add(new Action.Builder()
.key(KEY_CANCEL)
.title(getString(R.string.bluetooth_cancel))
.build());
break;
}
return actions;
}
private void dismiss() {
finish();
}
private void cancelPairing() {
if (DEBUG) {
Log.d(TAG, "cancelPairing");
}
mDevice.cancelPairingUserInput();
}
private void createUserEntryDialog() {
getFragmentManager().beginTransaction()
.replace(android.R.id.content, EntryDialogFragment.newInstance(mDevice, mType))
.commit();
}
private void createConfirmationDialog() {
// Build a Dialog activity view, with Action Fragment
final ArrayList<Action> actions = getActions();
final Fragment actionFragment = ActionFragment.newInstance(actions);
final Fragment contentFragment =
ConfirmationDialogFragment.newInstance(mDevice, mPairingKey, mType);
setContentAndActionFragments(contentFragment, actionFragment);
}
private void onPair(String value) {
if (DEBUG) {
Log.d(TAG, "onPair: " + value);
}
switch (mType) {
case BluetoothDevice.PAIRING_VARIANT_PIN:
byte[] pinBytes = BluetoothDevice.convertPinToBytes(value);
if (pinBytes == null) {
return;
}
mDevice.setPin(pinBytes);
mPairingInProgress = true;
break;
case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
try {
int passkey = Integer.parseInt(value);
mDevice.setPasskey(passkey);
mPairingInProgress = true;
} catch (NumberFormatException e) {
Log.d(TAG, "pass key " + value + " is not an integer");
}
break;
case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
case BluetoothDevice.PAIRING_VARIANT_CONSENT:
mDevice.setPairingConfirmation(true);
mPairingInProgress = true;
break;
case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
// Do nothing.
break;
case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
mDevice.setRemoteOutOfBandData();
mPairingInProgress = true;
break;
default:
Log.e(TAG, "Incorrect pairing type received");
}
}
@Override
public int getMetricsCategory() {
return MetricsProto.MetricsEvent.BLUETOOTH_DIALOG_FRAGMENT;
}
public static class EntryDialogFragment extends Fragment {
private static final String ARG_DEVICE = "ConfirmationDialogFragment.DEVICE";
private static final String ARG_TYPE = "ConfirmationDialogFragment.TYPE";
private BluetoothDevice mDevice;
private int mType;
public static EntryDialogFragment newInstance(BluetoothDevice device, int type) {
final EntryDialogFragment fragment = new EntryDialogFragment();
final Bundle b = new Bundle(2);
fragment.setArguments(b);
b.putParcelable(ARG_DEVICE, device);
b.putInt(ARG_TYPE, type);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle args = getArguments();
mDevice = args.getParcelable(ARG_DEVICE);
mType = args.getInt(ARG_TYPE);
}
@Override
public @Nullable View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.bt_pairing_passkey_entry, container, false);
final TextView titleText = (TextView) v.findViewById(R.id.title_text);
final EditText textInput = (EditText) v.findViewById(R.id.text_input);
textInput.setOnEditorActionListener(new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
String value = textInput.getText().toString();
if (actionId == EditorInfo.IME_ACTION_NEXT ||
(actionId == EditorInfo.IME_NULL &&
event.getAction() == KeyEvent.ACTION_DOWN)) {
((BluetoothPairingDialog)getActivity()).onPair(value);
}
return true;
}
});
final String instructions;
final int maxLength;
switch (mType) {
case BluetoothDevice.PAIRING_VARIANT_PIN:
instructions = getString(R.string.bluetooth_enter_pin_msg, mDevice.getName());
final TextView instructionText = (TextView) v.findViewById(R.id.hint_text);
instructionText.setText(getString(R.string.bluetooth_pin_values_hint));
// Maximum of 16 characters in a PIN
maxLength = BLUETOOTH_PIN_MAX_LENGTH;
textInput.setInputType(InputType.TYPE_CLASS_NUMBER);
break;
case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
instructions = getString(R.string.bluetooth_enter_passkey_msg,
mDevice.getName());
// Maximum of 6 digits for passkey
maxLength = BLUETOOTH_PASSKEY_MAX_LENGTH;
textInput.setInputType(InputType.TYPE_CLASS_TEXT);
break;
default:
throw new IllegalStateException("Incorrect pairing type for" +
" createPinEntryView: " + mType);
}
titleText.setText(Html.fromHtml(instructions));
textInput.setFilters(new InputFilter[]{new LengthFilter(maxLength)});
return v;
}
}
public static class ConfirmationDialogFragment extends Fragment {
private static final String ARG_DEVICE = "ConfirmationDialogFragment.DEVICE";
private static final String ARG_PAIRING_KEY = "ConfirmationDialogFragment.PAIRING_KEY";
private static final String ARG_TYPE = "ConfirmationDialogFragment.TYPE";
private BluetoothDevice mDevice;
private String mPairingKey;
private int mType;
public static ConfirmationDialogFragment newInstance(BluetoothDevice device,
String pairingKey, int type) {
final ConfirmationDialogFragment fragment = new ConfirmationDialogFragment();
final Bundle b = new Bundle(3);
b.putParcelable(ARG_DEVICE, device);
b.putString(ARG_PAIRING_KEY, pairingKey);
b.putInt(ARG_TYPE, type);
fragment.setArguments(b);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle args = getArguments();
mDevice = args.getParcelable(ARG_DEVICE);
mPairingKey = args.getString(ARG_PAIRING_KEY);
mType = args.getInt(ARG_TYPE);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.bt_pairing_passkey_display, container, false);
final TextView titleText = (TextView) v.findViewById(R.id.title);
final TextView instructionText = (TextView) v.findViewById(R.id.pairing_instructions);
titleText.setText(getString(R.string.bluetooth_pairing_request));
if (AccessibilityHelper.forceFocusableViews(getActivity())) {
titleText.setFocusable(true);
titleText.setFocusableInTouchMode(true);
instructionText.setFocusable(true);
instructionText.setFocusableInTouchMode(true);
}
final String instructions;
switch (mType) {
case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
instructions = getString(R.string.bluetooth_display_passkey_pin_msg,
mDevice.getName(), mPairingKey);
// Since its only a notification, send an OK to the framework,
// indicating that the dialog has been displayed.
if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) {
mDevice.setPairingConfirmation(true);
} else if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) {
byte[] pinBytes = BluetoothDevice.convertPinToBytes(mPairingKey);
mDevice.setPin(pinBytes);
}
break;
case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
instructions = getString(R.string.bluetooth_confirm_passkey_msg,
mDevice.getName(), mPairingKey);
break;
case BluetoothDevice.PAIRING_VARIANT_CONSENT:
case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
instructions = getString(R.string.bluetooth_incoming_pairing_msg,
mDevice.getName());
break;
default:
instructions = "";
}
instructionText.setText(Html.fromHtml(instructions));
return v;
}
}
}