blob: 91422036e7ea69f4e663e36fc182b57b98707966 [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.Activity;
import android.app.FragmentManager;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.hardware.input.InputManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import android.transition.TransitionManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import com.android.tv.settings.R;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* Activity for detecting and adding (pairing) new bluetooth devices.
*/
public class AddAccessoryActivity extends Activity implements BluetoothDevicePairer.EventListener {
private static final boolean DEBUG = false;
private static final String TAG = "AddAccessoryActivity";
private static final String ACTION_CONNECT_INPUT =
"com.google.android.intent.action.CONNECT_INPUT";
private static final String INTENT_EXTRA_NO_INPUT_MODE = "no_input_mode";
private static final String SAVED_STATE_PREFERENCE_FRAGMENT =
"AddAccessoryActivity.PREFERENCE_FRAGMENT";
private static final String SAVED_STATE_CONTENT_FRAGMENT =
"AddAccessoryActivity.CONTENT_FRAGMENT";
private static final String SAVED_STATE_BLUETOOTH_DEVICES =
"AddAccessoryActivity.BLUETOOTH_DEVICES";
private static final String ADDRESS_NONE = "NONE";
private static final int AUTOPAIR_COUNT = 10;
private static final int MSG_UPDATE_VIEW = 1;
private static final int MSG_REMOVE_CANCELED = 2;
private static final int MSG_PAIRING_COMPLETE = 3;
private static final int MSG_OP_TIMEOUT = 4;
private static final int MSG_RESTART = 5;
private static final int MSG_TRIGGER_SELECT_DOWN = 6;
private static final int MSG_TRIGGER_SELECT_UP = 7;
private static final int MSG_AUTOPAIR_TICK = 8;
private static final int MSG_START_AUTOPAIR_COUNTDOWN = 9;
private static final int CANCEL_MESSAGE_TIMEOUT = 3000;
private static final int DONE_MESSAGE_TIMEOUT = 3000;
private static final int PAIR_OPERATION_TIMEOUT = 120000;
private static final int CONNECT_OPERATION_TIMEOUT = 15000;
private static final int RESTART_DELAY = 3000;
private static final int LONG_PRESS_DURATION = 3000;
private static final int KEY_DOWN_TIME = 150;
private static final int TIME_TO_START_AUTOPAIR_COUNT = 5000;
private static final int EXIT_TIMEOUT_MILLIS = 90 * 1000;
private AddAccessoryPreferenceFragment mPreferenceFragment;
private AddAccessoryContentFragment mContentFragment;
// members related to Bluetooth pairing
private BluetoothDevicePairer mBluetoothPairer;
private int mPreviousStatus = BluetoothDevicePairer.STATUS_NONE;
private boolean mPairingSuccess = false;
private boolean mPairingBluetooth = false;
private List<BluetoothDevice> mBluetoothDevices;
private String mCancelledAddress = ADDRESS_NONE;
private String mCurrentTargetAddress = ADDRESS_NONE;
private String mCurrentTargetStatus = "";
private boolean mPairingInBackground = false;
private boolean mDone = false;
private boolean mHwKeyDown;
private boolean mHwKeyDidSelect;
private boolean mNoInputMode;
// Internal message handler
private final MessageHandler mMsgHandler = new MessageHandler();
private static class MessageHandler extends Handler {
private WeakReference<AddAccessoryActivity> mActivityRef = new WeakReference<>(null);
public void setActivity(AddAccessoryActivity activity) {
mActivityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
final AddAccessoryActivity activity = mActivityRef.get();
if (activity == null) {
return;
}
switch (msg.what) {
case MSG_UPDATE_VIEW:
activity.updateView();
break;
case MSG_REMOVE_CANCELED:
activity.mCancelledAddress = ADDRESS_NONE;
activity.updateView();
break;
case MSG_PAIRING_COMPLETE:
activity.finish();
break;
case MSG_OP_TIMEOUT:
activity.handlePairingTimeout();
break;
case MSG_RESTART:
if (activity.mBluetoothPairer != null) {
activity.mBluetoothPairer.start();
activity.mBluetoothPairer.cancelPairing();
}
break;
case MSG_TRIGGER_SELECT_DOWN:
activity.sendKeyEvent(KeyEvent.KEYCODE_DPAD_CENTER, true);
activity.mHwKeyDidSelect = true;
sendEmptyMessageDelayed(MSG_TRIGGER_SELECT_UP, KEY_DOWN_TIME);
activity.cancelPairingCountdown();
break;
case MSG_TRIGGER_SELECT_UP:
activity.sendKeyEvent(KeyEvent.KEYCODE_DPAD_CENTER, false);
break;
case MSG_START_AUTOPAIR_COUNTDOWN:
activity.setPairingText(
activity.getString(R.string.accessories_autopair_msg, AUTOPAIR_COUNT));
sendMessageDelayed(obtainMessage(MSG_AUTOPAIR_TICK,
AUTOPAIR_COUNT, 0, null), 1000);
break;
case MSG_AUTOPAIR_TICK:
int countToAutoPair = msg.arg1 - 1;
if (countToAutoPair <= 0) {
activity.setPairingText(null);
// AutoPair
activity.startAutoPairing();
} else {
activity.setPairingText(
activity.getString(R.string.accessories_autopair_msg,
countToAutoPair));
sendMessageDelayed(obtainMessage(MSG_AUTOPAIR_TICK,
countToAutoPair, 0, null), 1000);
}
break;
default:
super.handleMessage(msg);
}
}
}
private final Handler mAutoExitHandler = new Handler();
private final Runnable mAutoExitRunnable = this::finish;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.lb_dialog_fragment);
mMsgHandler.setActivity(this);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
mNoInputMode = getIntent().getBooleanExtra(INTENT_EXTRA_NO_INPUT_MODE, false);
mHwKeyDown = false;
if (savedInstanceState == null) {
mBluetoothDevices = new ArrayList<>();
} else {
mBluetoothDevices =
savedInstanceState.getParcelableArrayList(SAVED_STATE_BLUETOOTH_DEVICES);
}
final FragmentManager fm = getFragmentManager();
if (savedInstanceState == null) {
mPreferenceFragment = AddAccessoryPreferenceFragment.newInstance();
mContentFragment = AddAccessoryContentFragment.newInstance();
fm.beginTransaction()
.add(R.id.action_fragment, mPreferenceFragment)
.add(R.id.content_fragment, mContentFragment)
.commit();
} else {
mPreferenceFragment = (AddAccessoryPreferenceFragment)
fm.getFragment(savedInstanceState,
SAVED_STATE_PREFERENCE_FRAGMENT);
mContentFragment = (AddAccessoryContentFragment)
fm.getFragment(savedInstanceState,
SAVED_STATE_CONTENT_FRAGMENT);
}
rearrangeViews();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
getFragmentManager().putFragment(outState,
SAVED_STATE_PREFERENCE_FRAGMENT, mPreferenceFragment);
getFragmentManager().putFragment(outState,
SAVED_STATE_CONTENT_FRAGMENT, mContentFragment);
outState.putParcelableList(SAVED_STATE_BLUETOOTH_DEVICES, mBluetoothDevices);
}
@Override
protected void onStart() {
super.onStart();
if (DEBUG) {
Log.d(TAG, "onStart() mPairingInBackground = " + mPairingInBackground);
}
// Only do the following if we are not coming back to this activity from
// the Secure Pairing activity.
if (!mPairingInBackground) {
startBluetoothPairer();
}
mPairingInBackground = false;
}
@Override
public void onResume() {
super.onResume();
if (mNoInputMode) {
// Start timer count down for exiting activity.
if (DEBUG) Log.d(TAG, "starting auto-exit timer");
mAutoExitHandler.postDelayed(mAutoExitRunnable, EXIT_TIMEOUT_MILLIS);
}
}
@Override
public void onPause() {
super.onPause();
if (DEBUG) Log.d(TAG, "stopping auto-exit timer");
mAutoExitHandler.removeCallbacks(mAutoExitRunnable);
}
@Override
public void onStop() {
if (DEBUG) {
Log.d(TAG, "onStop()");
}
if (!mPairingBluetooth) {
stopBluetoothPairer();
mMsgHandler.removeCallbacksAndMessages(null);
} else {
// allow activity to remain in the background while we perform the
// BT Secure pairing.
mPairingInBackground = true;
}
super.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
stopBluetoothPairer();
mMsgHandler.removeCallbacksAndMessages(null);
}
@Override
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_HOME) {
if (mPairingBluetooth && !mDone) {
cancelBtPairing();
}
}
return super.onKeyUp(keyCode, event);
}
@Override
public void onNewIntent(Intent intent) {
if (ACTION_CONNECT_INPUT.equals(intent.getAction()) &&
(intent.getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) == 0) {
KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (event != null && event.getKeyCode() == KeyEvent.KEYCODE_PAIRING) {
if (event.getAction() == KeyEvent.ACTION_UP) {
onHwKeyEvent(false);
} else if (event.getAction() == KeyEvent.ACTION_DOWN) {
onHwKeyEvent(true);
}
}
} else {
setIntent(intent);
}
}
public void onActionClicked(String address) {
cancelPairingCountdown();
if (!mDone) {
btDeviceClicked(address);
}
}
// Events related to a device HW key
private void onHwKeyEvent(boolean keyDown) {
if (!mHwKeyDown) {
// HW key was in UP state before
if (keyDown) {
// Back key pressed down
mHwKeyDown = true;
mHwKeyDidSelect = false;
mMsgHandler.sendEmptyMessageDelayed(MSG_TRIGGER_SELECT_DOWN, LONG_PRESS_DURATION);
}
} else {
// HW key was in DOWN state before
if (!keyDown) {
// HW key released
mHwKeyDown = false;
mMsgHandler.removeMessages(MSG_TRIGGER_SELECT_DOWN);
if (!mHwKeyDidSelect) {
// key wasn't pressed long enough for selection, move selection
// to next item.
mPreferenceFragment.advanceSelection();
}
mHwKeyDidSelect = false;
}
}
}
private void sendKeyEvent(int keyCode, boolean down) {
InputManager iMgr = (InputManager) getSystemService(INPUT_SERVICE);
if (iMgr != null) {
long time = SystemClock.uptimeMillis();
KeyEvent evt = new KeyEvent(time, time,
down ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP,
keyCode, 0);
iMgr.injectInputEvent(evt, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
}
protected void updateView() {
if (mPreferenceFragment == null || isFinishing()) {
// view not yet ready, update will happen on first layout event
// or alternately we're done and don't need to do anything
return;
}
int prevNumDevices = mPreferenceFragment.getPreferenceScreen().getPreferenceCount();
mPreferenceFragment.updateList(mBluetoothDevices, mCurrentTargetAddress,
mCurrentTargetStatus, mCancelledAddress);
if (mNoInputMode) {
if (DEBUG) Log.d(TAG, "stopping auto-exit timer");
mAutoExitHandler.removeCallbacks(mAutoExitRunnable);
if (mBluetoothDevices.size() == 1 && prevNumDevices == 0) {
// first device added, start counter for autopair
mMsgHandler.sendEmptyMessageDelayed(MSG_START_AUTOPAIR_COUNTDOWN,
TIME_TO_START_AUTOPAIR_COUNT);
} else {
// Start timer count down for exiting activity.
if (DEBUG) Log.d(TAG, "starting auto-exit timer");
mAutoExitHandler.postDelayed(mAutoExitRunnable, EXIT_TIMEOUT_MILLIS);
if (mBluetoothDevices.size() > 1) {
// More than one device found, cancel auto pair
cancelPairingCountdown();
}
}
}
TransitionManager.beginDelayedTransition(findViewById(R.id.content_frame));
rearrangeViews();
}
private void rearrangeViews() {
final boolean empty = mBluetoothDevices.isEmpty();
final View contentView = findViewById(R.id.content_fragment);
final ViewGroup.LayoutParams contentLayoutParams = contentView.getLayoutParams();
contentLayoutParams.width = empty ? ViewGroup.LayoutParams.MATCH_PARENT :
getResources().getDimensionPixelSize(R.dimen.lb_content_section_width);
contentView.setLayoutParams(contentLayoutParams);
mContentFragment.setContentWidth(empty
? getResources().getDimensionPixelSize(R.dimen.progress_fragment_content_width)
: getResources().getDimensionPixelSize(R.dimen.bt_progress_width_narrow));
}
private void setPairingText(CharSequence text) {
if (mContentFragment != null) {
mContentFragment.setExtraText(text);
}
}
private void cancelPairingCountdown() {
// Cancel countdown
mMsgHandler.removeMessages(MSG_AUTOPAIR_TICK);
mMsgHandler.removeMessages(MSG_START_AUTOPAIR_COUNTDOWN);
setPairingText(null);
}
private void setTimeout(int timeout) {
cancelTimeout();
mMsgHandler.sendEmptyMessageDelayed(MSG_OP_TIMEOUT, timeout);
}
private void cancelTimeout() {
mMsgHandler.removeMessages(MSG_OP_TIMEOUT);
}
protected void startAutoPairing() {
if (mBluetoothDevices.size() > 0) {
onActionClicked(mBluetoothDevices.get(0).getAddress());
}
}
private void btDeviceClicked(String clickedAddress) {
if (mBluetoothPairer != null && !mBluetoothPairer.isInProgress()) {
if (mBluetoothPairer.getStatus() == BluetoothDevicePairer.STATUS_WAITING_TO_PAIR &&
mBluetoothPairer.getTargetDevice() != null) {
cancelBtPairing();
} else {
if (DEBUG) {
Log.d(TAG, "Looking for " + clickedAddress +
" in available devices to start pairing");
}
for (BluetoothDevice target : mBluetoothDevices) {
if (target.getAddress().equalsIgnoreCase(clickedAddress)) {
if (DEBUG) {
Log.d(TAG, "Found it!");
}
mCancelledAddress = ADDRESS_NONE;
setPairingBluetooth(true);
mBluetoothPairer.startPairing(target);
break;
}
}
}
}
}
private void cancelBtPairing() {
// cancel current request to pair
if (mBluetoothPairer != null) {
if (mBluetoothPairer.getTargetDevice() != null) {
mCancelledAddress = mBluetoothPairer.getTargetDevice().getAddress();
} else {
mCancelledAddress = ADDRESS_NONE;
}
mBluetoothPairer.cancelPairing();
}
mPairingSuccess = false;
setPairingBluetooth(false);
mMsgHandler.sendEmptyMessageDelayed(MSG_REMOVE_CANCELED,
CANCEL_MESSAGE_TIMEOUT);
}
private void setPairingBluetooth(boolean pairing) {
if (mPairingBluetooth != pairing) {
mPairingBluetooth = pairing;
}
}
private void startBluetoothPairer() {
stopBluetoothPairer();
mBluetoothPairer = new BluetoothDevicePairer(this, this);
mBluetoothPairer.start();
mBluetoothPairer.disableAutoPairing();
mPairingSuccess = false;
statusChanged();
}
private void stopBluetoothPairer() {
if (mBluetoothPairer != null) {
mBluetoothPairer.setListener(null);
mBluetoothPairer.dispose();
mBluetoothPairer = null;
}
}
private String getMessageForStatus(int status) {
final int msgId;
String msg;
switch (status) {
case BluetoothDevicePairer.STATUS_WAITING_TO_PAIR:
case BluetoothDevicePairer.STATUS_PAIRING:
msgId = R.string.accessory_state_pairing;
break;
case BluetoothDevicePairer.STATUS_CONNECTING:
msgId = R.string.accessory_state_connecting;
break;
case BluetoothDevicePairer.STATUS_ERROR:
msgId = R.string.accessory_state_error;
break;
default:
return "";
}
msg = getString(msgId);
return msg;
}
@Override
public void statusChanged() {
if (mBluetoothPairer == null) return;
int numDevices = mBluetoothPairer.getAvailableDevices().size();
int status = mBluetoothPairer.getStatus();
int oldStatus = mPreviousStatus;
mPreviousStatus = status;
String address = mBluetoothPairer.getTargetDevice() == null ? ADDRESS_NONE :
mBluetoothPairer.getTargetDevice().getAddress();
if (DEBUG) {
String state = "?";
switch (status) {
case BluetoothDevicePairer.STATUS_NONE:
state = "BluetoothDevicePairer.STATUS_NONE";
break;
case BluetoothDevicePairer.STATUS_SCANNING:
state = "BluetoothDevicePairer.STATUS_SCANNING";
break;
case BluetoothDevicePairer.STATUS_WAITING_TO_PAIR:
state = "BluetoothDevicePairer.STATUS_WAITING_TO_PAIR";
break;
case BluetoothDevicePairer.STATUS_PAIRING:
state = "BluetoothDevicePairer.STATUS_PAIRING";
break;
case BluetoothDevicePairer.STATUS_CONNECTING:
state = "BluetoothDevicePairer.STATUS_CONNECTING";
break;
case BluetoothDevicePairer.STATUS_ERROR:
state = "BluetoothDevicePairer.STATUS_ERROR";
break;
}
long time = mBluetoothPairer.getNextStageTime() - SystemClock.elapsedRealtime();
Log.d(TAG, "Update received, number of devices:" + numDevices + " state: " +
state + " target device: " + address + " time to next event: " + time);
}
mBluetoothDevices.clear();
mBluetoothDevices.addAll(mBluetoothPairer.getAvailableDevices());
cancelTimeout();
switch (status) {
case BluetoothDevicePairer.STATUS_NONE:
// if we just connected to something or just tried to connect
// to something, restart scanning just in case the user wants
// to pair another device.
if (oldStatus == BluetoothDevicePairer.STATUS_CONNECTING) {
if (mPairingSuccess) {
// Pairing complete
mCurrentTargetStatus = getString(R.string.accessory_state_paired);
mMsgHandler.sendEmptyMessage(MSG_UPDATE_VIEW);
mMsgHandler.sendEmptyMessageDelayed(MSG_PAIRING_COMPLETE,
DONE_MESSAGE_TIMEOUT);
mDone = true;
// Done, return here and just wait for the message
// to close the activity
return;
}
if (DEBUG) {
Log.d(TAG, "Invalidating and restarting.");
}
mBluetoothPairer.invalidateDevice(mBluetoothPairer.getTargetDevice());
mBluetoothPairer.start();
mBluetoothPairer.cancelPairing();
setPairingBluetooth(false);
// if this looks like a successful connection run, reflect
// this in the UI, otherwise use the default message
if (!mPairingSuccess && BluetoothDevicePairer.hasValidInputDevice(this)) {
mPairingSuccess = true;
}
}
break;
case BluetoothDevicePairer.STATUS_SCANNING:
mPairingSuccess = false;
break;
case BluetoothDevicePairer.STATUS_WAITING_TO_PAIR:
break;
case BluetoothDevicePairer.STATUS_PAIRING:
// reset the pairing success value since this is now a new
// pairing run
mPairingSuccess = true;
setTimeout(PAIR_OPERATION_TIMEOUT);
break;
case BluetoothDevicePairer.STATUS_CONNECTING:
setTimeout(CONNECT_OPERATION_TIMEOUT);
break;
case BluetoothDevicePairer.STATUS_ERROR:
mPairingSuccess = false;
setPairingBluetooth(false);
if (mNoInputMode) {
clearDeviceList();
}
break;
}
mCurrentTargetAddress = address;
mCurrentTargetStatus = getMessageForStatus(status);
mMsgHandler.sendEmptyMessage(MSG_UPDATE_VIEW);
}
private void clearDeviceList() {
mBluetoothDevices.clear();
mBluetoothPairer.clearDeviceList();
}
private void handlePairingTimeout() {
if (mPairingInBackground) {
finish();
} else {
// Either Pairing or Connecting timeout out.
// Display error message and post delayed message to the scanning process.
mPairingSuccess = false;
if (mBluetoothPairer != null) {
mBluetoothPairer.cancelPairing();
}
mCurrentTargetStatus = getString(R.string.accessory_state_error);
mMsgHandler.sendEmptyMessage(MSG_UPDATE_VIEW);
mMsgHandler.sendEmptyMessageDelayed(MSG_RESTART, RESTART_DELAY);
}
}
List<BluetoothDevice> getBluetoothDevices() {
return mBluetoothDevices;
}
String getCurrentTargetAddress() {
return mCurrentTargetAddress;
}
String getCurrentTargetStatus() {
return mCurrentTargetStatus;
}
String getCancelledAddress() {
return mCancelledAddress;
}
}