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