blob: 7974dfa8057099a641a52bdaf06721e6174e528f [file] [log] [blame]
/*
* Copyright (C) 2012 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.nfc.handover;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.Toast;
import com.android.nfc.handover.HandoverManager.HandoverPowerManager;
import com.android.nfc.R;
/**
* Connects / Disconnects from a Bluetooth headset (or any device that
* might implement BT HSP, HFP or A2DP sink) when touched with NFC.
*
* This object is created on an NFC interaction, and determines what
* sequence of Bluetooth actions to take, and executes them. It is not
* designed to be re-used after the sequence has completed or timed out.
* Subsequent NFC interactions should use new objects.
*
* TODO: UI review
*/
public class BluetoothHeadsetHandover {
static final String TAG = HandoverManager.TAG;
static final boolean DBG = HandoverManager.DBG;
static final String ACTION_ALLOW_CONNECT = "com.android.nfc.handover.action.ALLOW_CONNECT";
static final String ACTION_DENY_CONNECT = "com.android.nfc.handover.action.DENY_CONNECT";
static final int TIMEOUT_MS = 20000;
static final int STATE_INIT = 0;
static final int STATE_TURNING_ON = 1;
static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 2;
static final int STATE_BONDING = 3;
static final int STATE_CONNECTING = 4;
static final int STATE_DISCONNECTING = 5;
static final int STATE_COMPLETE = 6;
static final int RESULT_PENDING = 0;
static final int RESULT_CONNECTED = 1;
static final int RESULT_DISCONNECTED = 2;
static final int ACTION_DISCONNECT = 1;
static final int ACTION_CONNECT = 2;
static final int MSG_TIMEOUT = 1;
final Context mContext;
final BluetoothDevice mDevice;
final String mName;
final HandoverPowerManager mHandoverPowerManager;
final BluetoothA2dp mA2dp;
final BluetoothHeadset mHeadset;
final Callback mCallback;
// only used on main thread
int mAction;
int mState;
int mHfpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING
int mA2dpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING
public interface Callback {
public void onBluetoothHeadsetHandoverComplete(boolean connected);
}
public BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name,
HandoverPowerManager powerManager, BluetoothA2dp a2dp, BluetoothHeadset headset,
Callback callback) {
checkMainThread(); // mHandler must get get constructed on Main Thread for toasts to work
mContext = context;
mDevice = device;
mName = name;
mHandoverPowerManager = powerManager;
mA2dp = a2dp;
mHeadset = headset;
mCallback = callback;
mState = STATE_INIT;
}
/**
* Main entry point. This method is usually called after construction,
* to begin the BT sequence. Must be called on Main thread.
*/
public void start() {
checkMainThread();
if (mState != STATE_INIT) return;
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(ACTION_ALLOW_CONNECT);
filter.addAction(ACTION_DENY_CONNECT);
mContext.registerReceiver(mReceiver, filter);
if (mA2dp.getConnectedDevices().contains(mDevice) ||
mHeadset.getConnectedDevices().contains(mDevice)) {
Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName);
mAction = ACTION_DISCONNECT;
} else {
Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName);
mAction = ACTION_CONNECT;
}
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS);
nextStep();
}
/**
* Called to execute next step in state machine
*/
void nextStep() {
if (mAction == ACTION_CONNECT) {
nextStepConnect();
} else {
nextStepDisconnect();
}
}
void nextStepDisconnect() {
switch (mState) {
case STATE_INIT:
mState = STATE_DISCONNECTING;
if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_DISCONNECTED) {
mHfpResult = RESULT_PENDING;
mHeadset.disconnect(mDevice);
} else {
mHfpResult = RESULT_DISCONNECTED;
}
if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_DISCONNECTED) {
mA2dpResult = RESULT_PENDING;
mA2dp.disconnect(mDevice);
} else {
mA2dpResult = RESULT_DISCONNECTED;
}
if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
toast(mContext.getString(R.string.disconnecting_headset ) + " " +
mName + "...");
break;
}
// fall-through
case STATE_DISCONNECTING:
if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
// still disconnecting
break;
}
if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) {
toast(mContext.getString(R.string.disconnected_headset) + " " + mName);
}
complete(false);
break;
}
}
void nextStepConnect() {
switch (mState) {
case STATE_INIT:
if (!mHandoverPowerManager.isBluetoothEnabled()) {
if (mHandoverPowerManager.enableBluetooth()) {
// Bluetooth is being enabled
mState = STATE_TURNING_ON;
} else {
toast(mContext.getString(R.string.failed_to_enable_bt));
complete(false);
}
break;
}
// fall-through
case STATE_TURNING_ON:
if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
requestPairConfirmation();
mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
break;
}
// fall-through
case STATE_WAITING_FOR_BOND_CONFIRMATION:
if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
startBonding();
break;
}
// fall-through
case STATE_BONDING:
// Bluetooth Profile service will correctly serialize
// HFP then A2DP connect
mState = STATE_CONNECTING;
if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
mHfpResult = RESULT_PENDING;
mHeadset.connect(mDevice);
} else {
mHfpResult = RESULT_CONNECTED;
}
if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
mA2dpResult = RESULT_PENDING;
mA2dp.connect(mDevice);
} else {
mA2dpResult = RESULT_CONNECTED;
}
if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
toast(mContext.getString(R.string.connecting_headset) + " " + mName + "...");
break;
}
// fall-through
case STATE_CONNECTING:
if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
// another connection type still pending
break;
}
if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) {
// we'll take either as success
toast(mContext.getString(R.string.connected_headset) + " " + mName);
if (mA2dpResult == RESULT_CONNECTED) startTheMusic();
complete(true);
} else {
toast (mContext.getString(R.string.connect_headset_failed) + " " + mName);
complete(false);
}
break;
}
}
void startBonding() {
mState = STATE_BONDING;
toast(mContext.getString(R.string.pairing_headset) + " " + mName + "...");
if (!mDevice.createBond()) {
toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName);
complete(false);
}
}
void handleIntent(Intent intent) {
String action = intent.getAction();
if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action) && mState == STATE_TURNING_ON) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
if (state == BluetoothAdapter.STATE_ON) {
nextStepConnect();
} else if (state == BluetoothAdapter.STATE_OFF) {
toast(mContext.getString(R.string.failed_to_enable_bt));
complete(false);
}
return;
}
// Everything else requires the device to match...
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (!mDevice.equals(device)) return;
if (ACTION_ALLOW_CONNECT.equals(action)) {
nextStepConnect();
} else if (ACTION_DENY_CONNECT.equals(action)) {
complete(false);
} else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action) && mState == STATE_BONDING) {
int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothAdapter.ERROR);
if (bond == BluetoothDevice.BOND_BONDED) {
nextStepConnect();
} else if (bond == BluetoothDevice.BOND_NONE) {
toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName);
complete(false);
}
} else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
(mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
if (state == BluetoothProfile.STATE_CONNECTED) {
mHfpResult = RESULT_CONNECTED;
nextStep();
} else if (state == BluetoothProfile.STATE_DISCONNECTED) {
mHfpResult = RESULT_DISCONNECTED;
nextStep();
}
} else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
(mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
if (state == BluetoothProfile.STATE_CONNECTED) {
mA2dpResult = RESULT_CONNECTED;
nextStep();
} else if (state == BluetoothProfile.STATE_DISCONNECTED) {
mA2dpResult = RESULT_DISCONNECTED;
nextStep();
}
}
}
void complete(boolean connected) {
if (DBG) Log.d(TAG, "complete()");
mState = STATE_COMPLETE;
mContext.unregisterReceiver(mReceiver);
mHandler.removeMessages(MSG_TIMEOUT);
mCallback.onBluetoothHeadsetHandoverComplete(connected);
}
void toast(CharSequence text) {
Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show();
}
void startTheMusic() {
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_MEDIA_PLAY));
mContext.sendOrderedBroadcast(intent, null);
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP,
KeyEvent.KEYCODE_MEDIA_PLAY));
mContext.sendOrderedBroadcast(intent, null);
}
void requestPairConfirmation() {
Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class);
dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
mContext.startActivity(dialogIntent);
}
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_TIMEOUT:
if (mState == STATE_COMPLETE) return;
Log.i(TAG, "Timeout completing BT handover");
complete(false);
break;
}
}
};
final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
handleIntent(intent);
}
};
static void checkMainThread() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalThreadStateException("must be called on main thread");
}
}
}