| /* |
| * 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"); |
| } |
| } |
| } |