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 17package com.android.nfc.handover; 18 19import android.app.Service; 20import android.bluetooth.BluetoothAdapter; 21import android.bluetooth.BluetoothDevice; 22import android.content.BroadcastReceiver; 23import android.content.Context; 24import android.content.Intent; 25import android.content.IntentFilter; 26import android.media.AudioManager; 27import android.media.SoundPool; 28import android.net.Uri; 29import android.nfc.NfcAdapter; 30import android.os.Bundle; 31import android.os.Handler; 32import android.os.IBinder; 33import android.os.Message; 34import android.os.Messenger; 35import android.os.RemoteException; 36import android.util.Log; 37import android.util.Pair; 38 39import com.android.nfc.R; 40 41import java.io.File; 42import java.util.HashMap; 43import java.util.Iterator; 44import java.util.LinkedList; 45import java.util.Map; 46import java.util.Queue; 47 48public class HandoverService extends Service implements HandoverTransfer.Callback, 49 BluetoothPeripheralHandover.Callback { 50 51 static final String TAG = "HandoverService"; 52 static final boolean DBG = true; 53 54 static final int MSG_REGISTER_CLIENT = 0; 55 static final int MSG_DEREGISTER_CLIENT = 1; 56 static final int MSG_START_INCOMING_TRANSFER = 2; 57 static final int MSG_START_OUTGOING_TRANSFER = 3; 58 static final int MSG_PERIPHERAL_HANDOVER = 4; 59 static final int MSG_PAUSE_POLLING = 5; 60 61 62 static final String BUNDLE_TRANSFER = "transfer"; 63 64 static final String EXTRA_PERIPHERAL_DEVICE = "device"; 65 static final String EXTRA_PERIPHERAL_NAME = "headsetname"; 66 static final String EXTRA_PERIPHERAL_TRANSPORT = "transporttype"; 67 68 public static final String ACTION_CANCEL_HANDOVER_TRANSFER = 69 "com.android.nfc.handover.action.CANCEL_HANDOVER_TRANSFER"; 70 71 public static final String EXTRA_INCOMING = 72 "com.android.nfc.handover.extra.INCOMING"; 73 74 public static final String ACTION_HANDOVER_STARTED = 75 "android.nfc.handover.intent.action.HANDOVER_STARTED"; 76 77 public static final String ACTION_TRANSFER_PROGRESS = 78 "android.nfc.handover.intent.action.TRANSFER_PROGRESS"; 79 80 public static final String ACTION_TRANSFER_DONE = 81 "android.nfc.handover.intent.action.TRANSFER_DONE"; 82 83 public static final String EXTRA_TRANSFER_STATUS = 84 "android.nfc.handover.intent.extra.TRANSFER_STATUS"; 85 86 public static final String EXTRA_TRANSFER_MIMETYPE = 87 "android.nfc.handover.intent.extra.TRANSFER_MIME_TYPE"; 88 89 public static final String EXTRA_ADDRESS = 90 "android.nfc.handover.intent.extra.ADDRESS"; 91 92 public static final String EXTRA_TRANSFER_DIRECTION = 93 "android.nfc.handover.intent.extra.TRANSFER_DIRECTION"; 94 95 public static final String EXTRA_TRANSFER_ID = 96 "android.nfc.handover.intent.extra.TRANSFER_ID"; 97 98 public static final String EXTRA_TRANSFER_PROGRESS = 99 "android.nfc.handover.intent.extra.TRANSFER_PROGRESS"; 100 101 public static final String EXTRA_TRANSFER_URI = 102 "android.nfc.handover.intent.extra.TRANSFER_URI"; 103 104 public static final String EXTRA_OBJECT_COUNT = 105 "android.nfc.handover.intent.extra.OBJECT_COUNT"; 106 107 public static final String EXTRA_HANDOVER_DEVICE_TYPE = 108 "android.nfc.handover.intent.extra.HANDOVER_DEVICE_TYPE"; 109 110 public static final int DIRECTION_INCOMING = 0; 111 public static final int DIRECTION_OUTGOING = 1; 112 113 public static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0; 114 public static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1; 115 116 // permission needed to be able to receive handover status requests 117 public static final String HANDOVER_STATUS_PERMISSION = 118 "android.permission.NFC_HANDOVER_STATUS"; 119 120 // Amount of time to pause polling when connecting to peripherals 121 private static final int PAUSE_POLLING_TIMEOUT_MS = 35000; 122 public static final int PAUSE_DELAY_MILLIS = 300; 123 124 // Variables below only accessed on main thread 125 final Queue<BluetoothOppHandover> mPendingOutTransfers; 126 final HashMap<Pair<String, Boolean>, HandoverTransfer> mBluetoothTransfers; 127 final Messenger mMessenger; 128 129 SoundPool mSoundPool; 130 int mSuccessSound; 131 132 BluetoothAdapter mBluetoothAdapter; 133 NfcAdapter mNfcAdapter; 134 Messenger mClient; 135 Handler mHandler; 136 BluetoothPeripheralHandover mBluetoothPeripheralHandover; 137 boolean mBluetoothHeadsetConnected; 138 boolean mBluetoothEnabledByNfc; 139 140 private HandoverTransfer mWifiTransfer; 141 142 class MessageHandler extends Handler { 143 @Override 144 public void handleMessage(Message msg) { 145 switch (msg.what) { 146 case MSG_REGISTER_CLIENT: 147 mClient = msg.replyTo; 148 // Restore state from previous instance 149 mBluetoothEnabledByNfc = msg.arg1 != 0; 150 mBluetoothHeadsetConnected = msg.arg2 != 0; 151 break; 152 case MSG_DEREGISTER_CLIENT: 153 mClient = null; 154 break; 155 case MSG_START_INCOMING_TRANSFER: 156 doIncomingTransfer(msg); 157 break; 158 case MSG_START_OUTGOING_TRANSFER: 159 doOutgoingTransfer(msg); 160 break; 161 case MSG_PERIPHERAL_HANDOVER: 162 doPeripheralHandover(msg); 163 break; 164 case MSG_PAUSE_POLLING: 165 mNfcAdapter.pausePolling(PAUSE_POLLING_TIMEOUT_MS); 166 break; 167 } 168 } 169 170 } 171 172 final BroadcastReceiver mHandoverStatusReceiver = new BroadcastReceiver() { 173 @Override 174 public void onReceive(Context context, Intent intent) { 175 String action = intent.getAction(); 176 int deviceType = intent.getIntExtra(EXTRA_HANDOVER_DEVICE_TYPE, 177 HandoverTransfer.DEVICE_TYPE_BLUETOOTH); 178 179 if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { 180 handleBluetoothStateChanged(intent); 181 } else if (action.equals(ACTION_CANCEL_HANDOVER_TRANSFER)) { 182 handleCancelTransfer(intent, deviceType); 183 } else if (action.equals(ACTION_TRANSFER_PROGRESS) || 184 action.equals(ACTION_TRANSFER_DONE) || 185 action.equals(ACTION_HANDOVER_STARTED)) { 186 handleTransferEvent(intent, deviceType); 187 } 188 } 189 }; 190 191 public HandoverService() { 192 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 193 mPendingOutTransfers = new LinkedList<BluetoothOppHandover>(); 194 mBluetoothTransfers = new HashMap<Pair<String, Boolean>, HandoverTransfer>(); 195 mHandler = new MessageHandler(); 196 mMessenger = new Messenger(mHandler); 197 mBluetoothHeadsetConnected = false; 198 mBluetoothEnabledByNfc = false; 199 } 200 201 @Override 202 public void onCreate() { 203 super.onCreate(); 204 205 mSoundPool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0); 206 mSuccessSound = mSoundPool.load(this, R.raw.end, 1); 207 mNfcAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext()); 208 209 IntentFilter filter = new IntentFilter(ACTION_TRANSFER_DONE); 210 filter.addAction(ACTION_TRANSFER_PROGRESS); 211 filter.addAction(ACTION_CANCEL_HANDOVER_TRANSFER); 212 filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 213 filter.addAction(ACTION_HANDOVER_STARTED); 214 registerReceiver(mHandoverStatusReceiver, filter, HANDOVER_STATUS_PERMISSION, mHandler); 215 } 216 217 @Override 218 public void onDestroy() { 219 super.onDestroy(); 220 if (mSoundPool != null) { 221 mSoundPool.release(); 222 } 223 unregisterReceiver(mHandoverStatusReceiver); 224 } 225 226 void doOutgoingTransfer(Message msg) { 227 Bundle msgData = msg.getData(); 228 229 msgData.setClassLoader(getClassLoader()); 230 PendingHandoverTransfer pendingTransfer = msgData.getParcelable(BUNDLE_TRANSFER); 231 createHandoverTransfer(pendingTransfer); 232 233 if (pendingTransfer.deviceType == HandoverTransfer.DEVICE_TYPE_BLUETOOTH) { 234 // Create the actual bluetooth transfer 235 236 BluetoothOppHandover handover = new BluetoothOppHandover(HandoverService.this, 237 pendingTransfer.remoteDevice, pendingTransfer.uris, 238 pendingTransfer.remoteActivating); 239 if (mBluetoothAdapter.isEnabled()) { 240 // Start the transfer 241 handover.start(); 242 } else { 243 if (!enableBluetooth()) { 244 Log.e(TAG, "Error enabling Bluetooth."); 245 notifyClientTransferComplete(pendingTransfer.id); 246 return; 247 } 248 if (DBG) Log.d(TAG, "Queueing out transfer " + Integer.toString(pendingTransfer.id)); 249 mPendingOutTransfers.add(handover); 250 // Queue the transfer and enable Bluetooth - when it is enabled 251 // the transfer will be started. 252 } 253 } 254 } 255 256 void doIncomingTransfer(Message msg) { 257 Bundle msgData = msg.getData(); 258 259 msgData.setClassLoader(getClassLoader()); 260 PendingHandoverTransfer pendingTransfer = msgData.getParcelable(BUNDLE_TRANSFER); 261 if (pendingTransfer.deviceType == HandoverTransfer.DEVICE_TYPE_BLUETOOTH && 262 !mBluetoothAdapter.isEnabled() && !enableBluetooth()) { 263 Log.e(TAG, "Error enabling Bluetooth."); 264 notifyClientTransferComplete(pendingTransfer.id); 265 return; 266 } 267 createHandoverTransfer(pendingTransfer); 268 // Remote device will connect and finish the transfer 269 } 270 271 void doPeripheralHandover(Message msg) { 272 Bundle msgData = msg.getData(); 273 BluetoothDevice device = msgData.getParcelable(EXTRA_PERIPHERAL_DEVICE); 274 String name = msgData.getString(EXTRA_PERIPHERAL_NAME); 275 int transport = msgData.getInt(EXTRA_PERIPHERAL_TRANSPORT); 276 if (mBluetoothPeripheralHandover != null) { 277 Log.d(TAG, "Ignoring pairing request, existing handover in progress."); 278 return; 279 } 280 mBluetoothPeripheralHandover = new BluetoothPeripheralHandover(HandoverService.this, 281 device, name, transport, HandoverService.this); 282 // TODO: figure out a way to disable polling without deactivating current target 283 if (transport == BluetoothDevice.TRANSPORT_LE) { 284 mHandler.sendMessageDelayed( 285 mHandler.obtainMessage(MSG_PAUSE_POLLING), PAUSE_DELAY_MILLIS); 286 } 287 if (mBluetoothAdapter.isEnabled()) { 288 if (!mBluetoothPeripheralHandover.start()) { 289 mNfcAdapter.resumePolling(); 290 } 291 } else { 292 // Once BT is enabled, the headset pairing will be started 293 294 if (!enableBluetooth()) { 295 Log.e(TAG, "Error enabling Bluetooth."); 296 mBluetoothPeripheralHandover = null; 297 } 298 } 299 } 300 301 void startPendingTransfers() { 302 while (!mPendingOutTransfers.isEmpty()) { 303 BluetoothOppHandover handover = mPendingOutTransfers.remove(); 304 handover.start(); 305 } 306 } 307 308 boolean enableBluetooth() { 309 if (!mBluetoothAdapter.isEnabled()) { 310 mBluetoothEnabledByNfc = true; 311 return mBluetoothAdapter.enableNoAutoConnect(); 312 } 313 return true; 314 } 315 316 void disableBluetoothIfNeeded() { 317 if (!mBluetoothEnabledByNfc) return; 318 319 if (mBluetoothTransfers.size() == 0 && !mBluetoothHeadsetConnected) { 320 mBluetoothAdapter.disable(); 321 mBluetoothEnabledByNfc = false; 322 } 323 } 324 325 void createHandoverTransfer(PendingHandoverTransfer pendingTransfer) { 326 HandoverTransfer transfer; 327 String macAddress; 328 329 if (pendingTransfer.deviceType == HandoverTransfer.DEVICE_TYPE_BLUETOOTH) { 330 macAddress = pendingTransfer.remoteDevice.getAddress(); 331 transfer = maybeCreateHandoverTransfer(macAddress, 332 pendingTransfer.incoming, pendingTransfer); 333 } else { 334 Log.e(TAG, "Invalid device type [" + pendingTransfer.deviceType + "] received."); 335 return; 336 } 337 338 if (transfer != null) { 339 transfer.updateNotification(); 340 } 341 } 342 343 HandoverTransfer maybeCreateHandoverTransfer(String address, boolean incoming, 344 PendingHandoverTransfer pendingTransfer) { 345 HandoverTransfer transfer; 346 Pair<String, Boolean> key = new Pair<String, Boolean>(address, incoming); 347 348 if (mBluetoothTransfers.containsKey(key)) { 349 transfer = mBluetoothTransfers.get(key); 350 if (!transfer.isRunning()) { 351 mBluetoothTransfers.remove(key); // new one created below 352 } else { 353 // There is already a transfer running to this 354 // device - it will automatically get combined 355 // with the existing transfer. 356 notifyClientTransferComplete(pendingTransfer.id); 357 return null; 358 } 359 } else { 360 transfer = new HandoverTransfer(this, this, pendingTransfer); 361 } 362 363 mBluetoothTransfers.put(key, transfer); 364 return transfer; 365 } 366 367 368 HandoverTransfer findHandoverTransfer(String macAddress, boolean incoming) { 369 Pair<String, Boolean> key = new Pair<String, Boolean>(macAddress, incoming); 370 if (mBluetoothTransfers.containsKey(key)) { 371 HandoverTransfer transfer = mBluetoothTransfers.get(key); 372 if (transfer.isRunning()) { 373 return transfer; 374 } 375 } 376 377 return null; 378 } 379 380 @Override 381 public IBinder onBind(Intent intent) { 382 return mMessenger.getBinder(); 383 } 384 385 private void handleTransferEvent(Intent intent, int deviceType) { 386 String action = intent.getAction(); 387 int direction = intent.getIntExtra(EXTRA_TRANSFER_DIRECTION, -1); 388 int id = intent.getIntExtra(EXTRA_TRANSFER_ID, -1); 389 if (action.equals(ACTION_HANDOVER_STARTED)) { 390 // This is always for incoming transfers 391 direction = DIRECTION_INCOMING; 392 } 393 String sourceAddress = intent.getStringExtra(EXTRA_ADDRESS); 394 395 if (direction == -1 || sourceAddress == null) return; 396 boolean incoming = (direction == DIRECTION_INCOMING); 397 398 HandoverTransfer transfer = 399 findHandoverTransfer(sourceAddress, incoming); 400 if (transfer == null) { 401 // There is no transfer running for this source address; most likely 402 // the transfer was cancelled. We need to tell BT OPP to stop transferring. 403 if (id != -1) { 404 if (deviceType == HandoverTransfer.DEVICE_TYPE_BLUETOOTH) { 405 if (DBG) Log.d(TAG, "Didn't find transfer, stopping"); 406 Intent cancelIntent = new Intent( 407 "android.btopp.intent.action.STOP_HANDOVER_TRANSFER"); 408 cancelIntent.putExtra(EXTRA_TRANSFER_ID, id); 409 sendBroadcast(cancelIntent); 410 } 411 } 412 return; 413 } 414 415 transfer.setBluetoothTransferId(id); 416 417 if (action.equals(ACTION_TRANSFER_DONE)) { 418 int handoverStatus = intent.getIntExtra(EXTRA_TRANSFER_STATUS, 419 HANDOVER_TRANSFER_STATUS_FAILURE); 420 if (handoverStatus == HANDOVER_TRANSFER_STATUS_SUCCESS) { 421 String uriString = intent.getStringExtra(EXTRA_TRANSFER_URI); 422 String mimeType = intent.getStringExtra(EXTRA_TRANSFER_MIMETYPE); 423 Uri uri = Uri.parse(uriString); 424 if (uri != null && uri.getScheme() == null) { 425 uri = Uri.fromFile(new File(uri.getPath())); 426 } 427 transfer.finishTransfer(true, uri, mimeType); 428 } else { 429 transfer.finishTransfer(false, null, null); 430 } 431 } else if (action.equals(ACTION_TRANSFER_PROGRESS)) { 432 float progress = intent.getFloatExtra(EXTRA_TRANSFER_PROGRESS, 0.0f); 433 transfer.updateFileProgress(progress); 434 } else if (action.equals(ACTION_HANDOVER_STARTED)) { 435 int count = intent.getIntExtra(EXTRA_OBJECT_COUNT, 0); 436 if (count > 0) { 437 transfer.setObjectCount(count); 438 } 439 } 440 } 441 442 private void handleCancelTransfer(Intent intent, int deviceType) { 443 String sourceAddress = intent.getStringExtra(EXTRA_ADDRESS); 444 int direction = intent.getIntExtra(EXTRA_INCOMING, -1); 445 446 if (direction == -1) { 447 return; 448 } 449 450 boolean incoming = direction == DIRECTION_INCOMING; 451 HandoverTransfer transfer = findHandoverTransfer(sourceAddress, incoming); 452 453 if (transfer != null) { 454 if (DBG) Log.d(TAG, "Cancelling transfer " + Integer.toString(transfer.mTransferId)); 455 transfer.cancel(); 456 } 457 } 458 459 private void handleBluetoothStateChanged(Intent intent) { 460 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 461 BluetoothAdapter.ERROR); 462 if (state == BluetoothAdapter.STATE_ON) { 463 // If there is a pending device pairing, start it 464 if (mBluetoothPeripheralHandover != null && 465 !mBluetoothPeripheralHandover.hasStarted()) { 466 if (!mBluetoothPeripheralHandover.start()) { 467 mNfcAdapter.resumePolling(); 468 } 469 } 470 471 // Start any pending file transfers 472 startPendingTransfers(); 473 } else if (state == BluetoothAdapter.STATE_OFF) { 474 mBluetoothEnabledByNfc = false; 475 mBluetoothHeadsetConnected = false; 476 } 477 } 478 479 void notifyClientTransferComplete(int transferId) { 480 if (mClient != null) { 481 Message msg = Message.obtain(null, HandoverManager.MSG_HANDOVER_COMPLETE); 482 msg.arg1 = transferId; 483 try { 484 mClient.send(msg); 485 } catch (RemoteException e) { 486 // Ignore 487 } 488 } 489 } 490 491 @Override 492 public boolean onUnbind(Intent intent) { 493 // prevent any future callbacks to the client, no rebind call needed. 494 mClient = null; 495 return false; 496 } 497 498 @Override 499 public void onTransferComplete(HandoverTransfer transfer, boolean success) { 500 // Called on the main thread 501 502 // First, remove the transfer from our list 503 synchronized (this) { 504 if (mWifiTransfer == transfer) { 505 mWifiTransfer = null; 506 } 507 } 508 509 if (mWifiTransfer == null) { 510 Iterator it = mBluetoothTransfers.entrySet().iterator(); 511 while (it.hasNext()) { 512 Map.Entry hashPair = (Map.Entry)it.next(); 513 HandoverTransfer transferEntry = (HandoverTransfer) hashPair.getValue(); 514 if (transferEntry == transfer) { 515 it.remove(); 516 } 517 } 518 } 519 520 // Notify any clients of the service 521 notifyClientTransferComplete(transfer.getTransferId()); 522 523 // Play success sound 524 if (success) { 525 mSoundPool.play(mSuccessSound, 1.0f, 1.0f, 0, 0, 1.0f); 526 } else { 527 if (DBG) Log.d(TAG, "Transfer failed, final state: " + 528 Integer.toString(transfer.mState)); 529 } 530 disableBluetoothIfNeeded(); 531 } 532 533 @Override 534 public void onBluetoothPeripheralHandoverComplete(boolean connected) { 535 // Called on the main thread 536 int transport = mBluetoothPeripheralHandover.mTransport; 537 mBluetoothPeripheralHandover = null; 538 mBluetoothHeadsetConnected = connected; 539 540 // <hack> resume polling immediately if the connection failed, 541 // otherwise just wait for polling to come back up after the timeout 542 // This ensures we don't disconnect if the user left the volantis 543 // on the tag after pairing completed, which results in automatic 544 // disconnection </hack> 545 if (transport == BluetoothDevice.TRANSPORT_LE && !connected) { 546 if (mHandler.hasMessages(MSG_PAUSE_POLLING)) { 547 mHandler.removeMessages(MSG_PAUSE_POLLING); 548 } 549 550 // do this unconditionally as the polling could have been paused as we were removing 551 // the message in the handler. It's a no-op if polling is already enabled. 552 mNfcAdapter.resumePolling(); 553 } 554 555 if (mClient != null) { 556 Message msg = Message.obtain(null, 557 connected ? HandoverManager.MSG_HEADSET_CONNECTED 558 : HandoverManager.MSG_HEADSET_NOT_CONNECTED); 559 msg.arg1 = mBluetoothEnabledByNfc ? 1 : 0; 560 try { 561 mClient.send(msg); 562 } catch (RemoteException e) { 563 // Ignore 564 } 565 } 566 disableBluetoothIfNeeded(); 567 } 568} 569