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.R; 36 37 /** 38 * Connects / Disconnects from a Bluetooth headset (or any device that 39 * might implement BT HSP, HFP or A2DP sink) when touched with NFC. 40 * 41 * This object is created on an NFC interaction, and determines what 42 * sequence of Bluetooth actions to take, and executes them. It is not 43 * designed to be re-used after the sequence has completed or timed out. 44 * Subsequent NFC interactions should use new objects. 45 * 46 */ 47 public class BluetoothHeadsetHandover implements BluetoothProfile.ServiceListener { 48 static final String TAG = HandoverManager.TAG; 49 static final boolean DBG = HandoverManager.DBG; 50 51 static final String ACTION_ALLOW_CONNECT = "com.android.nfc.handover.action.ALLOW_CONNECT"; 52 static final String ACTION_DENY_CONNECT = "com.android.nfc.handover.action.DENY_CONNECT"; 53 54 static final int TIMEOUT_MS = 20000; 55 56 static final int STATE_INIT = 0; 57 static final int STATE_WAITING_FOR_PROXIES = 1; 58 static final int STATE_INIT_COMPLETE = 2; 59 static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 3; 60 static final int STATE_BONDING = 4; 61 static final int STATE_CONNECTING = 5; 62 static final int STATE_DISCONNECTING = 6; 63 static final int STATE_COMPLETE = 7; 64 65 static final int RESULT_PENDING = 0; 66 static final int RESULT_CONNECTED = 1; 67 static final int RESULT_DISCONNECTED = 2; 68 69 static final int ACTION_INIT = 0; 70 static final int ACTION_DISCONNECT = 1; 71 static final int ACTION_CONNECT = 2; 72 73 static final int MSG_TIMEOUT = 1; 74 static final int MSG_NEXT_STEP = 2; 75 76 final Context mContext; 77 final BluetoothDevice mDevice; 78 final String mName; 79 final Callback mCallback; 80 final BluetoothAdapter mBluetoothAdapter; 81 82 final Object mLock = new Object(); 83 84 // only used on main thread 85 int mAction; 86 int mState; 87 int mHfpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING 88 int mA2dpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING 89 90 // protected by mLock 91 BluetoothA2dp mA2dp; 92 BluetoothHeadset mHeadset; 93 94 public interface Callback { onBluetoothHeadsetHandoverComplete(boolean connected)95 public void onBluetoothHeadsetHandoverComplete(boolean connected); 96 } 97 BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name, Callback callback)98 public BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name, 99 Callback callback) { 100 checkMainThread(); // mHandler must get get constructed on Main Thread for toasts to work 101 mContext = context; 102 mDevice = device; 103 mName = name; 104 mCallback = callback; 105 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 106 107 mState = STATE_INIT; 108 } 109 hasStarted()110 public boolean hasStarted() { 111 return mState != STATE_INIT; 112 } 113 114 /** 115 * Main entry point. This method is usually called after construction, 116 * to begin the BT sequence. Must be called on Main thread. 117 */ start()118 public void start() { 119 checkMainThread(); 120 if (mState != STATE_INIT) return; 121 if (mBluetoothAdapter == null) return; 122 123 IntentFilter filter = new IntentFilter(); 124 filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 125 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 126 filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); 127 filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 128 filter.addAction(ACTION_ALLOW_CONNECT); 129 filter.addAction(ACTION_DENY_CONNECT); 130 131 mContext.registerReceiver(mReceiver, filter); 132 133 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS); 134 mAction = ACTION_INIT; 135 nextStep(); 136 } 137 138 /** 139 * Called to execute next step in state machine 140 */ nextStep()141 void nextStep() { 142 if (mAction == ACTION_INIT) { 143 nextStepInit(); 144 } else if (mAction == ACTION_CONNECT) { 145 nextStepConnect(); 146 } else { 147 nextStepDisconnect(); 148 } 149 } 150 151 /* 152 * Enables bluetooth and gets the profile proxies 153 */ nextStepInit()154 void nextStepInit() { 155 switch (mState) { 156 case STATE_INIT: 157 if (mA2dp == null || mHeadset == null) { 158 mState = STATE_WAITING_FOR_PROXIES; 159 if (!getProfileProxys()) { 160 complete(false); 161 } 162 break; 163 } 164 // fall-through 165 case STATE_WAITING_FOR_PROXIES: 166 mState = STATE_INIT_COMPLETE; 167 // Check connected devices and see if we need to disconnect 168 synchronized(mLock) { 169 if (mA2dp.getConnectedDevices().contains(mDevice) || 170 mHeadset.getConnectedDevices().contains(mDevice)) { 171 Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName); 172 mAction = ACTION_DISCONNECT; 173 } else { 174 Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName); 175 mAction = ACTION_CONNECT; 176 } 177 } 178 nextStep(); 179 } 180 181 } 182 nextStepDisconnect()183 void nextStepDisconnect() { 184 switch (mState) { 185 case STATE_INIT_COMPLETE: 186 mState = STATE_DISCONNECTING; 187 synchronized (mLock) { 188 if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_DISCONNECTED) { 189 mHfpResult = RESULT_PENDING; 190 mHeadset.disconnect(mDevice); 191 } else { 192 mHfpResult = RESULT_DISCONNECTED; 193 } 194 if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_DISCONNECTED) { 195 mA2dpResult = RESULT_PENDING; 196 mA2dp.disconnect(mDevice); 197 } else { 198 mA2dpResult = RESULT_DISCONNECTED; 199 } 200 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { 201 toast(mContext.getString(R.string.disconnecting_headset ) + " " + 202 mName + "..."); 203 break; 204 } 205 } 206 // fall-through 207 case STATE_DISCONNECTING: 208 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { 209 // still disconnecting 210 break; 211 } 212 if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) { 213 toast(mContext.getString(R.string.disconnected_headset) + " " + mName); 214 } 215 complete(false); 216 break; 217 } 218 219 } 220 getProfileProxys()221 boolean getProfileProxys() { 222 if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET)) 223 return false; 224 225 if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP)) 226 return false; 227 228 return true; 229 } 230 nextStepConnect()231 void nextStepConnect() { 232 switch (mState) { 233 case STATE_INIT_COMPLETE: 234 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) { 235 requestPairConfirmation(); 236 mState = STATE_WAITING_FOR_BOND_CONFIRMATION; 237 238 break; 239 } 240 // fall-through 241 case STATE_WAITING_FOR_BOND_CONFIRMATION: 242 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) { 243 startBonding(); 244 break; 245 } 246 // fall-through 247 case STATE_BONDING: 248 // Bluetooth Profile service will correctly serialize 249 // HFP then A2DP connect 250 mState = STATE_CONNECTING; 251 synchronized (mLock) { 252 if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) { 253 mHfpResult = RESULT_PENDING; 254 mHeadset.connect(mDevice); 255 } else { 256 mHfpResult = RESULT_CONNECTED; 257 } 258 if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) { 259 mA2dpResult = RESULT_PENDING; 260 mA2dp.connect(mDevice); 261 } else { 262 mA2dpResult = RESULT_CONNECTED; 263 } 264 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { 265 toast(mContext.getString(R.string.connecting_headset) + " " + mName + "..."); 266 break; 267 } 268 } 269 // fall-through 270 case STATE_CONNECTING: 271 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { 272 // another connection type still pending 273 break; 274 } 275 if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) { 276 // we'll take either as success 277 toast(mContext.getString(R.string.connected_headset) + " " + mName); 278 if (mA2dpResult == RESULT_CONNECTED) startTheMusic(); 279 complete(true); 280 } else { 281 toast (mContext.getString(R.string.connect_headset_failed) + " " + mName); 282 complete(false); 283 } 284 break; 285 } 286 } 287 startBonding()288 void startBonding() { 289 mState = STATE_BONDING; 290 toast(mContext.getString(R.string.pairing_headset) + " " + mName + "..."); 291 if (!mDevice.createBond()) { 292 toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName); 293 complete(false); 294 } 295 } 296 handleIntent(Intent intent)297 void handleIntent(Intent intent) { 298 String action = intent.getAction(); 299 // Everything requires the device to match... 300 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 301 if (!mDevice.equals(device)) return; 302 303 if (ACTION_ALLOW_CONNECT.equals(action)) { 304 nextStepConnect(); 305 } else if (ACTION_DENY_CONNECT.equals(action)) { 306 complete(false); 307 } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action) && mState == STATE_BONDING) { 308 int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 309 BluetoothAdapter.ERROR); 310 if (bond == BluetoothDevice.BOND_BONDED) { 311 nextStepConnect(); 312 } else if (bond == BluetoothDevice.BOND_NONE) { 313 toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName); 314 complete(false); 315 } 316 } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) && 317 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) { 318 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR); 319 if (state == BluetoothProfile.STATE_CONNECTED) { 320 mHfpResult = RESULT_CONNECTED; 321 nextStep(); 322 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 323 mHfpResult = RESULT_DISCONNECTED; 324 nextStep(); 325 } 326 } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action) && 327 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) { 328 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR); 329 if (state == BluetoothProfile.STATE_CONNECTED) { 330 mA2dpResult = RESULT_CONNECTED; 331 nextStep(); 332 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 333 mA2dpResult = RESULT_DISCONNECTED; 334 nextStep(); 335 } 336 } 337 } 338 complete(boolean connected)339 void complete(boolean connected) { 340 if (DBG) Log.d(TAG, "complete()"); 341 mState = STATE_COMPLETE; 342 mContext.unregisterReceiver(mReceiver); 343 mHandler.removeMessages(MSG_TIMEOUT); 344 synchronized (mLock) { 345 if (mA2dp != null) { 346 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dp); 347 } 348 if (mHeadset != null) { 349 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadset); 350 } 351 mA2dp = null; 352 mHeadset = null; 353 } 354 mCallback.onBluetoothHeadsetHandoverComplete(connected); 355 } 356 toast(CharSequence text)357 void toast(CharSequence text) { 358 Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show(); 359 } 360 startTheMusic()361 void startTheMusic() { 362 Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); 363 intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, 364 KeyEvent.KEYCODE_MEDIA_PLAY)); 365 mContext.sendOrderedBroadcast(intent, null); 366 intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, 367 KeyEvent.KEYCODE_MEDIA_PLAY)); 368 mContext.sendOrderedBroadcast(intent, null); 369 } 370 requestPairConfirmation()371 void requestPairConfirmation() { 372 Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class); 373 dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 374 dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); 375 376 mContext.startActivity(dialogIntent); 377 } 378 379 final Handler mHandler = new Handler() { 380 @Override 381 public void handleMessage(Message msg) { 382 switch (msg.what) { 383 case MSG_TIMEOUT: 384 if (mState == STATE_COMPLETE) return; 385 Log.i(TAG, "Timeout completing BT handover"); 386 complete(false); 387 break; 388 case MSG_NEXT_STEP: 389 nextStep(); 390 break; 391 } 392 } 393 }; 394 395 final BroadcastReceiver mReceiver = new BroadcastReceiver() { 396 @Override 397 public void onReceive(Context context, Intent intent) { 398 handleIntent(intent); 399 } 400 }; 401 checkMainThread()402 static void checkMainThread() { 403 if (Looper.myLooper() != Looper.getMainLooper()) { 404 throw new IllegalThreadStateException("must be called on main thread"); 405 } 406 } 407 408 @Override onServiceConnected(int profile, BluetoothProfile proxy)409 public void onServiceConnected(int profile, BluetoothProfile proxy) { 410 synchronized (mLock) { 411 switch (profile) { 412 case BluetoothProfile.HEADSET: 413 mHeadset = (BluetoothHeadset) proxy; 414 if (mA2dp != null) { 415 mHandler.sendEmptyMessage(MSG_NEXT_STEP); 416 } 417 break; 418 case BluetoothProfile.A2DP: 419 mA2dp = (BluetoothA2dp) proxy; 420 if (mHeadset != null) { 421 mHandler.sendEmptyMessage(MSG_NEXT_STEP); 422 } 423 break; 424 } 425 } 426 } 427 428 @Override onServiceDisconnected(int profile)429 public void onServiceDisconnected(int profile) { 430 // We can ignore these 431 } 432 } 433