blob: 09cdf04d39bef4120ba8aba6bbe27c828d645e96 [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.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHidHost;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.OobData;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.media.session.MediaSessionLegacyHelper;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelUuid;
import android.provider.Settings;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.Toast;
import com.android.nfc.R;
/**
* Connects / Disconnects from a Bluetooth headset (or any device that
* might implement BT HSP, HFP, A2DP, or HOGP 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.
*
*/
public class BluetoothPeripheralHandover implements BluetoothProfile.ServiceListener {
static final String TAG = "BluetoothPeripheralHandover";
static final boolean DBG = false;
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 String ACTION_TIMEOUT_CONNECT = "com.android.nfc.handover.action.TIMEOUT_CONNECT";
static final int TIMEOUT_MS = 20000;
static final int RETRY_PAIRING_WAIT_TIME_MS = 2000;
static final int RETRY_CONNECT_WAIT_TIME_MS = 5000;
static final int STATE_INIT = 0;
static final int STATE_WAITING_FOR_PROXIES = 1;
static final int STATE_INIT_COMPLETE = 2;
static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 3;
static final int STATE_BONDING = 4;
static final int STATE_CONNECTING = 5;
static final int STATE_DISCONNECTING = 6;
static final int STATE_COMPLETE = 7;
static final int RESULT_PENDING = 0;
static final int RESULT_CONNECTED = 1;
static final int RESULT_DISCONNECTED = 2;
static final int ACTION_INIT = 0;
static final int ACTION_DISCONNECT = 1;
static final int ACTION_CONNECT = 2;
static final int MSG_TIMEOUT = 1;
static final int MSG_NEXT_STEP = 2;
static final int MSG_RETRY = 3;
static final int MAX_RETRY_COUNT = 3;
final Context mContext;
final BluetoothDevice mDevice;
final String mName;
final Callback mCallback;
final BluetoothAdapter mBluetoothAdapter;
final int mTransport;
final boolean mProvisioning;
final AudioManager mAudioManager;
final Object mLock = new Object();
// 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
int mHidResult;
int mRetryCount;
OobData mOobData;
boolean mIsHeadsetAvailable;
boolean mIsA2dpAvailable;
boolean mIsMusicActive;
// protected by mLock
BluetoothA2dp mA2dp;
BluetoothHeadset mHeadset;
BluetoothHidHost mInput;
public interface Callback {
public void onBluetoothPeripheralHandoverComplete(boolean connected);
}
public BluetoothPeripheralHandover(Context context, BluetoothDevice device, String name,
int transport, OobData oobData, ParcelUuid[] uuids, BluetoothClass btClass,
Callback callback) {
checkMainThread(); // mHandler must get get constructed on Main Thread for toasts to work
mContext = context;
mDevice = device;
mName = name;
mTransport = transport;
mOobData = oobData;
mCallback = callback;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
ContentResolver contentResolver = mContext.getContentResolver();
mProvisioning = Settings.Global.getInt(contentResolver,
Settings.Global.DEVICE_PROVISIONED, 0) == 0;
mIsHeadsetAvailable = hasHeadsetCapability(uuids, btClass);
mIsA2dpAvailable = hasA2dpCapability(uuids, btClass);
// Capability information is from NDEF optional field, then it might be empty.
// If all capabilities indicate false, try to connect Headset and A2dp just in case.
if (!mIsHeadsetAvailable && !mIsA2dpAvailable) {
mIsHeadsetAvailable = true;
mIsA2dpAvailable = true;
}
mAudioManager = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE);
mState = STATE_INIT;
}
public boolean hasStarted() {
return 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 boolean start() {
checkMainThread();
if (mState != STATE_INIT || mBluetoothAdapter == null
|| (mProvisioning && mTransport != BluetoothDevice.TRANSPORT_LE)) {
return false;
}
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(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(ACTION_ALLOW_CONNECT);
filter.addAction(ACTION_DENY_CONNECT);
mContext.registerReceiver(mReceiver, filter);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS);
mAction = ACTION_INIT;
mRetryCount = 0;
nextStep();
return true;
}
/**
* Called to execute next step in state machine
*/
void nextStep() {
if (mAction == ACTION_INIT) {
nextStepInit();
} else if (mAction == ACTION_CONNECT) {
nextStepConnect();
} else {
nextStepDisconnect();
}
}
/*
* Enables bluetooth and gets the profile proxies
*/
void nextStepInit() {
switch (mState) {
case STATE_INIT:
if (mA2dp == null || mHeadset == null || mInput == null) {
mState = STATE_WAITING_FOR_PROXIES;
if (!getProfileProxys()) {
complete(false);
}
break;
}
// fall-through
case STATE_WAITING_FOR_PROXIES:
mState = STATE_INIT_COMPLETE;
// Check connected devices and see if we need to disconnect
synchronized(mLock) {
if (mTransport == BluetoothDevice.TRANSPORT_LE) {
if (mInput.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;
}
} else {
if (mA2dp.getConnectedDevices().contains(mDevice) ||
mHeadset.getConnectedDevices().contains(mDevice)) {
Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName);
mAction = ACTION_DISCONNECT;
} else {
// Check if each profile of the device is disabled or not
if (mHeadset.getPriority(mDevice) == BluetoothProfile.PRIORITY_OFF) {
mIsHeadsetAvailable = false;
}
if (mA2dp.getPriority(mDevice) == BluetoothProfile.PRIORITY_OFF) {
mIsA2dpAvailable = false;
}
if (!mIsHeadsetAvailable && !mIsA2dpAvailable) {
Log.i(TAG, "Both Headset and A2DP profiles are unavailable");
complete(false);
break;
}
Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName);
mAction = ACTION_CONNECT;
if (mIsA2dpAvailable) {
mIsMusicActive = mAudioManager.isMusicActive();
}
}
}
}
nextStep();
}
}
void nextStepDisconnect() {
switch (mState) {
case STATE_INIT_COMPLETE:
mState = STATE_DISCONNECTING;
synchronized (mLock) {
if (mTransport == BluetoothDevice.TRANSPORT_LE) {
if (mInput.getConnectionState(mDevice)
!= BluetoothProfile.STATE_DISCONNECTED) {
mHidResult = RESULT_PENDING;
mInput.disconnect(mDevice);
toast(getToastString(R.string.disconnecting_peripheral));
break;
} else {
mHidResult = RESULT_DISCONNECTED;
}
} else {
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(getToastString(R.string.disconnecting_peripheral));
break;
}
}
}
// fall-through
case STATE_DISCONNECTING:
if (mTransport == BluetoothDevice.TRANSPORT_LE) {
if (mHidResult == RESULT_DISCONNECTED) {
toast(getToastString(R.string.disconnected_peripheral));
complete(false);
}
break;
} else {
if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
// still disconnecting
break;
}
if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) {
toast(getToastString(R.string.disconnected_peripheral));
}
complete(false);
break;
}
}
}
private String getToastString(int resid) {
return mContext.getString(resid, mName != null ? mName : R.string.device);
}
boolean getProfileProxys() {
if (mTransport == BluetoothDevice.TRANSPORT_LE) {
if (!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HID_HOST))
return false;
} else {
if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET))
return false;
if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP))
return false;
}
return true;
}
void nextStepConnect() {
switch (mState) {
case STATE_INIT_COMPLETE:
if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
requestPairConfirmation();
mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
break;
}
if (mTransport == BluetoothDevice.TRANSPORT_LE) {
if (mDevice.getBondState() != BluetoothDevice.BOND_NONE) {
mDevice.removeBond();
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;
synchronized (mLock) {
if (mTransport == BluetoothDevice.TRANSPORT_LE) {
if (mInput.getConnectionState(mDevice)
!= BluetoothProfile.STATE_CONNECTED) {
mHidResult = RESULT_PENDING;
toast(getToastString(R.string.connecting_peripheral));
break;
} else {
mHidResult = RESULT_CONNECTED;
}
} else {
if (mHeadset.getConnectionState(mDevice) !=
BluetoothProfile.STATE_CONNECTED) {
if (mIsHeadsetAvailable) {
mHfpResult = RESULT_PENDING;
mHeadset.connect(mDevice);
} else {
mHfpResult = RESULT_DISCONNECTED;
}
} else {
mHfpResult = RESULT_CONNECTED;
}
if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
if (mIsA2dpAvailable) {
mA2dpResult = RESULT_PENDING;
mA2dp.connect(mDevice);
} else {
mA2dpResult = RESULT_DISCONNECTED;
}
} else {
mA2dpResult = RESULT_CONNECTED;
}
if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
if (mRetryCount == 0) {
toast(getToastString(R.string.connecting_peripheral));
}
if (mRetryCount < MAX_RETRY_COUNT) {
sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
break;
}
}
}
}
// fall-through
case STATE_CONNECTING:
if (mTransport == BluetoothDevice.TRANSPORT_LE) {
if (mHidResult == RESULT_PENDING) {
break;
} else if (mHidResult == RESULT_CONNECTED) {
toast(getToastString(R.string.connected_peripheral));
mDevice.setAlias(mName);
complete(true);
} else {
toast (getToastString(R.string.connect_peripheral_failed));
complete(false);
}
} else {
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(getToastString(R.string.connected_peripheral));
if (mA2dpResult == RESULT_CONNECTED) startTheMusic();
mDevice.setAlias(mName);
complete(true);
} else {
toast (getToastString(R.string.connect_peripheral_failed));
complete(false);
}
}
break;
}
}
void startBonding() {
mState = STATE_BONDING;
if (mRetryCount == 0) {
toast(getToastString(R.string.pairing_peripheral));
}
if (mOobData != null) {
if (!mDevice.createBondOutOfBand(mTransport, mOobData)) {
toast(getToastString(R.string.pairing_peripheral_failed));
complete(false);
}
} else if (!mDevice.createBond(mTransport)) {
toast(getToastString(R.string.pairing_peripheral_failed));
complete(false);
}
}
void handleIntent(Intent intent) {
String action = intent.getAction();
// Everything requires the device to match...
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (!mDevice.equals(device)) return;
if (ACTION_ALLOW_CONNECT.equals(action)) {
mHandler.removeMessages(MSG_TIMEOUT);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS);
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) {
mRetryCount = 0;
nextStepConnect();
} else if (bond == BluetoothDevice.BOND_NONE) {
if (mRetryCount < MAX_RETRY_COUNT) {
sendRetryMessage(RETRY_PAIRING_WAIT_TIME_MS);
} else {
toast(getToastString(R.string.pairing_peripheral_failed));
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) {
if (mAction == ACTION_CONNECT && mRetryCount < MAX_RETRY_COUNT) {
sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
} else {
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) {
if (mAction == ACTION_CONNECT && mRetryCount < MAX_RETRY_COUNT) {
sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
} else {
mA2dpResult = RESULT_DISCONNECTED;
nextStep();
}
}
} else if (BluetoothHidHost.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) {
mHidResult = RESULT_CONNECTED;
nextStep();
} else if (state == BluetoothProfile.STATE_DISCONNECTED) {
mHidResult = RESULT_DISCONNECTED;
nextStep();
}
}
}
void complete(boolean connected) {
if (DBG) Log.d(TAG, "complete()");
mState = STATE_COMPLETE;
mContext.unregisterReceiver(mReceiver);
mHandler.removeMessages(MSG_TIMEOUT);
mHandler.removeMessages(MSG_RETRY);
synchronized (mLock) {
if (mA2dp != null) {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dp);
}
if (mHeadset != null) {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadset);
}
if (mInput != null) {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HID_HOST, mInput);
}
mA2dp = null;
mHeadset = null;
mInput = null;
}
mCallback.onBluetoothPeripheralHandoverComplete(connected);
}
void toast(CharSequence text) {
Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show();
}
void startTheMusic() {
if (!mContext.getResources().getBoolean(R.bool.enable_auto_play) && !mIsMusicActive) {
return;
}
MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext);
if (helper != null) {
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
helper.sendMediaButtonEvent(keyEvent, false);
keyEvent = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY);
helper.sendMediaButtonEvent(keyEvent, false);
} else {
Log.w(TAG, "Unable to send media key event");
}
}
void requestPairConfirmation() {
Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class);
dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
dialogIntent.putExtra(BluetoothDevice.EXTRA_NAME, mName);
mContext.startActivity(dialogIntent);
}
boolean hasA2dpCapability(ParcelUuid[] uuids, BluetoothClass btClass) {
if (uuids != null) {
for (ParcelUuid uuid : uuids) {
if (BluetoothUuid.isAudioSink(uuid) || BluetoothUuid.isAdvAudioDist(uuid)) {
return true;
}
}
}
if (btClass != null && btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
return true;
}
return false;
}
boolean hasHeadsetCapability(ParcelUuid[] uuids, BluetoothClass btClass) {
if (uuids != null) {
for (ParcelUuid uuid : uuids) {
if (BluetoothUuid.isHandsfree(uuid) || BluetoothUuid.isHeadset(uuid)) {
return true;
}
}
}
if (btClass != null && btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
return true;
}
return false;
}
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");
if (mState == STATE_WAITING_FOR_BOND_CONFIRMATION) {
mContext.sendBroadcast(new Intent(ACTION_TIMEOUT_CONNECT));
} else if (mState == STATE_BONDING) {
toast(getToastString(R.string.pairing_peripheral_failed));
} else if (mState == STATE_CONNECTING) {
if (mHidResult == RESULT_PENDING) {
mHidResult = RESULT_DISCONNECTED;
}
if (mA2dpResult == RESULT_PENDING) {
mA2dpResult = RESULT_DISCONNECTED;
}
if (mHfpResult == RESULT_PENDING) {
mHfpResult = RESULT_DISCONNECTED;
}
// Check if any one profile is connected, then it takes as success
nextStepConnect();
break;
}
complete(false);
break;
case MSG_NEXT_STEP:
nextStep();
break;
case MSG_RETRY:
mHandler.removeMessages(MSG_RETRY);
if (mState == STATE_BONDING) {
mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
} else if (mState == STATE_CONNECTING) {
mState = STATE_BONDING;
}
mRetryCount++;
nextStepConnect();
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");
}
}
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
synchronized (mLock) {
switch (profile) {
case BluetoothProfile.HEADSET:
mHeadset = (BluetoothHeadset) proxy;
if (mA2dp != null) {
mHandler.sendEmptyMessage(MSG_NEXT_STEP);
}
break;
case BluetoothProfile.A2DP:
mA2dp = (BluetoothA2dp) proxy;
if (mHeadset != null) {
mHandler.sendEmptyMessage(MSG_NEXT_STEP);
}
break;
case BluetoothProfile.HID_HOST:
mInput = (BluetoothHidHost) proxy;
if (mInput != null) {
mHandler.sendEmptyMessage(MSG_NEXT_STEP);
}
break;
}
}
}
@Override
public void onServiceDisconnected(int profile) {
// We can ignore these
}
void sendRetryMessage(int waitTime) {
if (!mHandler.hasMessages(MSG_RETRY)) {
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY), waitTime);
}
}
}