1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.nfc.handover; 18 19 import android.bluetooth.BluetoothA2dp; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothHeadset; 23 import android.bluetooth.BluetoothProfile; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.os.Handler; 29 import android.os.Looper; 30 import android.os.Message; 31 import android.util.Log; 32 import android.view.KeyEvent; 33 import android.widget.Toast; 34 35 import com.android.nfc.handover.HandoverManager.HandoverPowerManager; 36 import com.android.nfc.R; 37 38 /** 39 * Connects / Disconnects from a Bluetooth headset (or any device that 40 * might implement BT HSP, HFP or A2DP sink) when touched with NFC. 41 * 42 * This object is created on an NFC interaction, and determines what 43 * sequence of Bluetooth actions to take, and executes them. It is not 44 * designed to be re-used after the sequence has completed or timed out. 45 * Subsequent NFC interactions should use new objects. 46 * 47 * TODO: UI review 48 */ 49 public class BluetoothHeadsetHandover { 50 static final String TAG = HandoverManager.TAG; 51 static final boolean DBG = HandoverManager.DBG; 52 53 static final String ACTION_ALLOW_CONNECT = "com.android.nfc.handover.action.ALLOW_CONNECT"; 54 static final String ACTION_DENY_CONNECT = "com.android.nfc.handover.action.DENY_CONNECT"; 55 56 static final int TIMEOUT_MS = 20000; 57 58 static final int STATE_INIT = 0; 59 static final int STATE_TURNING_ON = 1; 60 static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 2; 61 static final int STATE_BONDING = 3; 62 static final int STATE_CONNECTING = 4; 63 static final int STATE_DISCONNECTING = 5; 64 static final int STATE_COMPLETE = 6; 65 66 static final int RESULT_PENDING = 0; 67 static final int RESULT_CONNECTED = 1; 68 static final int RESULT_DISCONNECTED = 2; 69 70 static final int ACTION_DISCONNECT = 1; 71 static final int ACTION_CONNECT = 2; 72 73 static final int MSG_TIMEOUT = 1; 74 75 final Context mContext; 76 final BluetoothDevice mDevice; 77 final String mName; 78 final HandoverPowerManager mHandoverPowerManager; 79 final BluetoothA2dp mA2dp; 80 final BluetoothHeadset mHeadset; 81 final Callback mCallback; 82 83 // only used on main thread 84 int mAction; 85 int mState; 86 int mHfpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING 87 int mA2dpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING 88 89 public interface Callback { onBluetoothHeadsetHandoverComplete(boolean connected)90 public void onBluetoothHeadsetHandoverComplete(boolean connected); 91 } 92 BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name, HandoverPowerManager powerManager, BluetoothA2dp a2dp, BluetoothHeadset headset, Callback callback)93 public BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name, 94 HandoverPowerManager powerManager, BluetoothA2dp a2dp, BluetoothHeadset headset, 95 Callback callback) { 96 checkMainThread(); // mHandler must get get constructed on Main Thread for toasts to work 97 mContext = context; 98 mDevice = device; 99 mName = name; 100 mHandoverPowerManager = powerManager; 101 mA2dp = a2dp; 102 mHeadset = headset; 103 mCallback = callback; 104 mState = STATE_INIT; 105 } 106 107 /** 108 * Main entry point. This method is usually called after construction, 109 * to begin the BT sequence. Must be called on Main thread. 110 */ start()111 public void start() { 112 checkMainThread(); 113 if (mState != STATE_INIT) return; 114 115 IntentFilter filter = new IntentFilter(); 116 filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 117 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 118 filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); 119 filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 120 filter.addAction(ACTION_ALLOW_CONNECT); 121 filter.addAction(ACTION_DENY_CONNECT); 122 123 mContext.registerReceiver(mReceiver, filter); 124 125 if (mA2dp.getConnectedDevices().contains(mDevice) || 126 mHeadset.getConnectedDevices().contains(mDevice)) { 127 Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName); 128 mAction = ACTION_DISCONNECT; 129 } else { 130 Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName); 131 mAction = ACTION_CONNECT; 132 } 133 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS); 134 nextStep(); 135 } 136 137 /** 138 * Called to execute next step in state machine 139 */ nextStep()140 void nextStep() { 141 if (mAction == ACTION_CONNECT) { 142 nextStepConnect(); 143 } else { 144 nextStepDisconnect(); 145 } 146 } 147 nextStepDisconnect()148 void nextStepDisconnect() { 149 switch (mState) { 150 case STATE_INIT: 151 mState = STATE_DISCONNECTING; 152 if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_DISCONNECTED) { 153 mHfpResult = RESULT_PENDING; 154 mHeadset.disconnect(mDevice); 155 } else { 156 mHfpResult = RESULT_DISCONNECTED; 157 } 158 if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_DISCONNECTED) { 159 mA2dpResult = RESULT_PENDING; 160 mA2dp.disconnect(mDevice); 161 } else { 162 mA2dpResult = RESULT_DISCONNECTED; 163 } 164 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { 165 toast(mContext.getString(R.string.disconnecting_headset ) + " " + 166 mName + "..."); 167 break; 168 } 169 // fall-through 170 case STATE_DISCONNECTING: 171 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { 172 // still disconnecting 173 break; 174 } 175 if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) { 176 toast(mContext.getString(R.string.disconnected_headset) + " " + mName); 177 } 178 complete(false); 179 break; 180 } 181 } 182 nextStepConnect()183 void nextStepConnect() { 184 switch (mState) { 185 case STATE_INIT: 186 if (!mHandoverPowerManager.isBluetoothEnabled()) { 187 if (mHandoverPowerManager.enableBluetooth()) { 188 // Bluetooth is being enabled 189 mState = STATE_TURNING_ON; 190 } else { 191 toast(mContext.getString(R.string.failed_to_enable_bt)); 192 complete(false); 193 } 194 break; 195 } 196 // fall-through 197 case STATE_TURNING_ON: 198 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) { 199 requestPairConfirmation(); 200 mState = STATE_WAITING_FOR_BOND_CONFIRMATION; 201 break; 202 } 203 // fall-through 204 case STATE_WAITING_FOR_BOND_CONFIRMATION: 205 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) { 206 startBonding(); 207 break; 208 } 209 // fall-through 210 case STATE_BONDING: 211 // Bluetooth Profile service will correctly serialize 212 // HFP then A2DP connect 213 mState = STATE_CONNECTING; 214 if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) { 215 mHfpResult = RESULT_PENDING; 216 mHeadset.connect(mDevice); 217 } else { 218 mHfpResult = RESULT_CONNECTED; 219 } 220 if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) { 221 mA2dpResult = RESULT_PENDING; 222 mA2dp.connect(mDevice); 223 } else { 224 mA2dpResult = RESULT_CONNECTED; 225 } 226 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { 227 toast(mContext.getString(R.string.connecting_headset) + " " + mName + "..."); 228 break; 229 } 230 // fall-through 231 case STATE_CONNECTING: 232 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { 233 // another connection type still pending 234 break; 235 } 236 if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) { 237 // we'll take either as success 238 toast(mContext.getString(R.string.connected_headset) + " " + mName); 239 if (mA2dpResult == RESULT_CONNECTED) startTheMusic(); 240 complete(true); 241 } else { 242 toast (mContext.getString(R.string.connect_headset_failed) + " " + mName); 243 complete(false); 244 } 245 break; 246 } 247 } 248 startBonding()249 void startBonding() { 250 mState = STATE_BONDING; 251 toast(mContext.getString(R.string.pairing_headset) + " " + mName + "..."); 252 if (!mDevice.createBond()) { 253 toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName); 254 complete(false); 255 } 256 } 257 handleIntent(Intent intent)258 void handleIntent(Intent intent) { 259 String action = intent.getAction(); 260 if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action) && mState == STATE_TURNING_ON) { 261 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); 262 if (state == BluetoothAdapter.STATE_ON) { 263 nextStepConnect(); 264 } else if (state == BluetoothAdapter.STATE_OFF) { 265 toast(mContext.getString(R.string.failed_to_enable_bt)); 266 complete(false); 267 } 268 return; 269 } 270 271 // Everything else requires the device to match... 272 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 273 if (!mDevice.equals(device)) return; 274 275 if (ACTION_ALLOW_CONNECT.equals(action)) { 276 nextStepConnect(); 277 } else if (ACTION_DENY_CONNECT.equals(action)) { 278 complete(false); 279 } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action) && mState == STATE_BONDING) { 280 int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 281 BluetoothAdapter.ERROR); 282 if (bond == BluetoothDevice.BOND_BONDED) { 283 nextStepConnect(); 284 } else if (bond == BluetoothDevice.BOND_NONE) { 285 toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName); 286 complete(false); 287 } 288 } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) && 289 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) { 290 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR); 291 if (state == BluetoothProfile.STATE_CONNECTED) { 292 mHfpResult = RESULT_CONNECTED; 293 nextStep(); 294 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 295 mHfpResult = RESULT_DISCONNECTED; 296 nextStep(); 297 } 298 } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action) && 299 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) { 300 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR); 301 if (state == BluetoothProfile.STATE_CONNECTED) { 302 mA2dpResult = RESULT_CONNECTED; 303 nextStep(); 304 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 305 mA2dpResult = RESULT_DISCONNECTED; 306 nextStep(); 307 } 308 } 309 } 310 complete(boolean connected)311 void complete(boolean connected) { 312 if (DBG) Log.d(TAG, "complete()"); 313 mState = STATE_COMPLETE; 314 mContext.unregisterReceiver(mReceiver); 315 mHandler.removeMessages(MSG_TIMEOUT); 316 mCallback.onBluetoothHeadsetHandoverComplete(connected); 317 } 318 toast(CharSequence text)319 void toast(CharSequence text) { 320 Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show(); 321 } 322 startTheMusic()323 void startTheMusic() { 324 Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); 325 intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, 326 KeyEvent.KEYCODE_MEDIA_PLAY)); 327 mContext.sendOrderedBroadcast(intent, null); 328 intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, 329 KeyEvent.KEYCODE_MEDIA_PLAY)); 330 mContext.sendOrderedBroadcast(intent, null); 331 } 332 requestPairConfirmation()333 void requestPairConfirmation() { 334 Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class); 335 dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 336 337 dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); 338 339 mContext.startActivity(dialogIntent); 340 } 341 342 final Handler mHandler = new Handler() { 343 @Override 344 public void handleMessage(Message msg) { 345 switch (msg.what) { 346 case MSG_TIMEOUT: 347 if (mState == STATE_COMPLETE) return; 348 Log.i(TAG, "Timeout completing BT handover"); 349 complete(false); 350 break; 351 } 352 } 353 }; 354 355 final BroadcastReceiver mReceiver = new BroadcastReceiver() { 356 @Override 357 public void onReceive(Context context, Intent intent) { 358 handleIntent(intent); 359 } 360 }; 361 checkMainThread()362 static void checkMainThread() { 363 if (Looper.myLooper() != Looper.getMainLooper()) { 364 throw new IllegalThreadStateException("must be called on main thread"); 365 } 366 } 367 } 368