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 java.nio.BufferUnderflowException; 20 import java.nio.ByteBuffer; 21 import java.nio.charset.Charset; 22 import java.util.Arrays; 23 import java.util.HashMap; 24 import java.util.Random; 25 26 import android.bluetooth.BluetoothAdapter; 27 import android.bluetooth.BluetoothDevice; 28 import android.content.BroadcastReceiver; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.content.ServiceConnection; 34 import android.net.Uri; 35 import android.nfc.FormatException; 36 import android.nfc.NdefMessage; 37 import android.nfc.NdefRecord; 38 import android.os.Bundle; 39 import android.os.Handler; 40 import android.os.IBinder; 41 import android.os.Message; 42 import android.os.Messenger; 43 import android.os.RemoteException; 44 import android.os.UserHandle; 45 import android.util.Log; 46 47 /** 48 * Manages handover of NFC to other technologies. 49 */ 50 public class HandoverManager { 51 static final String TAG = "NfcHandover"; 52 static final boolean DBG = true; 53 54 static final String ACTION_WHITELIST_DEVICE = 55 "android.btopp.intent.action.WHITELIST_DEVICE"; 56 57 static final byte[] TYPE_NOKIA = "nokia.com:bt".getBytes(Charset.forName("US_ASCII")); 58 static final byte[] TYPE_BT_OOB = "application/vnd.bluetooth.ep.oob". 59 getBytes(Charset.forName("US_ASCII")); 60 61 static final byte[] RTD_COLLISION_RESOLUTION = {0x63, 0x72}; // "cr"; 62 63 static final int CARRIER_POWER_STATE_INACTIVE = 0; 64 static final int CARRIER_POWER_STATE_ACTIVE = 1; 65 static final int CARRIER_POWER_STATE_ACTIVATING = 2; 66 static final int CARRIER_POWER_STATE_UNKNOWN = 3; 67 68 static final int MSG_HANDOVER_COMPLETE = 0; 69 static final int MSG_HEADSET_CONNECTED = 1; 70 static final int MSG_HEADSET_NOT_CONNECTED = 2; 71 72 final Context mContext; 73 final BluetoothAdapter mBluetoothAdapter; 74 final Messenger mMessenger = new Messenger (new MessageHandler()); 75 76 final Object mLock = new Object(); 77 // Variables below synchronized on mLock 78 HashMap<Integer, PendingHandoverTransfer> mPendingTransfers; 79 boolean mBluetoothHeadsetConnected; 80 int mHandoverTransferId; 81 Messenger mService = null; 82 boolean mBound; 83 String mLocalBluetoothAddress; 84 boolean mEnabled; 85 86 static class BluetoothHandoverData { 87 public boolean valid = false; 88 public BluetoothDevice device; 89 public String name; 90 public boolean carrierActivating = false; 91 } 92 93 class MessageHandler extends Handler { 94 @Override 95 public void handleMessage(Message msg) { 96 synchronized (mLock) { 97 switch (msg.what) { 98 case MSG_HANDOVER_COMPLETE: 99 int transferId = msg.arg1; 100 Log.d(TAG, "Completed transfer id: " + Integer.toString(transferId)); 101 if (mPendingTransfers.containsKey(transferId)) { 102 mPendingTransfers.remove(transferId); 103 } else { 104 Log.e(TAG, "Could not find completed transfer id: " + Integer.toString(transferId)); 105 } 106 break; 107 case MSG_HEADSET_CONNECTED: 108 mBluetoothHeadsetConnected = true; 109 break; 110 case MSG_HEADSET_NOT_CONNECTED: 111 mBluetoothHeadsetConnected = false; 112 break; 113 default: 114 break; 115 } 116 } 117 } 118 }; 119 120 private ServiceConnection mConnection = new ServiceConnection() { 121 @Override 122 public void onServiceConnected(ComponentName name, IBinder service) { 123 synchronized (mLock) { 124 mService = new Messenger(service); 125 mBound = true; 126 // Register this client 127 Message msg = Message.obtain(null, HandoverService.MSG_REGISTER_CLIENT); 128 msg.replyTo = mMessenger; 129 try { 130 mService.send(msg); 131 } catch (RemoteException e) { 132 Log.e(TAG, "Failed to register client"); 133 } 134 } 135 } 136 137 @Override 138 public void onServiceDisconnected(ComponentName name) { 139 synchronized (mLock) { 140 if (mBound) { 141 try { 142 Message msg = Message.obtain(null, HandoverService.MSG_DEREGISTER_CLIENT); 143 msg.replyTo = mMessenger; 144 mService.send(msg); 145 } catch (RemoteException e) { 146 // Service may have crashed - ignore 147 } 148 } 149 mService = null; 150 mBound = false; 151 } 152 } 153 }; 154 155 public HandoverManager(Context context) { 156 mContext = context; 157 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 158 159 mPendingTransfers = new HashMap<Integer, PendingHandoverTransfer>(); 160 161 IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED); 162 mContext.registerReceiver(mReceiver, filter, null, null); 163 164 mContext.bindServiceAsUser(new Intent(mContext, HandoverService.class), mConnection, 165 Context.BIND_AUTO_CREATE, UserHandle.CURRENT); 166 mEnabled = true; 167 } 168 169 final BroadcastReceiver mReceiver = new BroadcastReceiver() { 170 @Override 171 public void onReceive(Context context, Intent intent) { 172 String action = intent.getAction(); 173 if (action.equals(Intent.ACTION_USER_SWITCHED)) { 174 // Re-bind a service for the current user 175 mContext.unbindService(mConnection); 176 mContext.bindServiceAsUser(new Intent(mContext, HandoverService.class), mConnection, 177 Context.BIND_AUTO_CREATE, UserHandle.CURRENT); 178 } 179 } 180 }; 181 182 static NdefRecord createCollisionRecord() { 183 byte[] random = new byte[2]; 184 new Random().nextBytes(random); 185 return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, RTD_COLLISION_RESOLUTION, null, random); 186 } 187 188 NdefRecord createBluetoothAlternateCarrierRecord(boolean activating) { 189 byte[] payload = new byte[4]; 190 payload[0] = (byte) (activating ? CARRIER_POWER_STATE_ACTIVATING : 191 CARRIER_POWER_STATE_ACTIVE); // Carrier Power State: Activating or active 192 payload[1] = 1; // length of carrier data reference 193 payload[2] = 'b'; // carrier data reference: ID for Bluetooth OOB data record 194 payload[3] = 0; // Auxiliary data reference count 195 return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_ALTERNATIVE_CARRIER, null, payload); 196 } 197 198 NdefRecord createBluetoothOobDataRecord() { 199 byte[] payload = new byte[8]; 200 // Note: this field should be little-endian per the BTSSP spec 201 // The Android 4.1 implementation used big-endian order here. 202 // No single Android implementation has ever interpreted this 203 // length field when parsing this record though. 204 payload[0] = (byte) (payload.length & 0xFF); 205 payload[1] = (byte) ((payload.length >> 8) & 0xFF); 206 207 synchronized (mLock) { 208 if (mLocalBluetoothAddress == null) { 209 mLocalBluetoothAddress = mBluetoothAdapter.getAddress(); 210 } 211 212 byte[] addressBytes = addressToReverseBytes(mLocalBluetoothAddress); 213 System.arraycopy(addressBytes, 0, payload, 2, 6); 214 } 215 216 return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, TYPE_BT_OOB, new byte[]{'b'}, payload); 217 } 218 219 public void setEnabled(boolean enabled) { 220 synchronized (mLock) { 221 mEnabled = enabled; 222 } 223 } 224 public boolean isHandoverSupported() { 225 return (mBluetoothAdapter != null); 226 } 227 228 public NdefMessage createHandoverRequestMessage() { 229 if (mBluetoothAdapter == null) return null; 230 231 return new NdefMessage(createHandoverRequestRecord(), createBluetoothOobDataRecord()); 232 } 233 234 NdefMessage createHandoverSelectMessage(boolean activating) { 235 return new NdefMessage(createHandoverSelectRecord(activating), createBluetoothOobDataRecord()); 236 } 237 238 NdefRecord createHandoverSelectRecord(boolean activating) { 239 NdefMessage nestedMessage = new NdefMessage(createBluetoothAlternateCarrierRecord(activating)); 240 byte[] nestedPayload = nestedMessage.toByteArray(); 241 242 ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1); 243 payload.put((byte)0x12); // connection handover v1.2 244 payload.put(nestedPayload); 245 246 byte[] payloadBytes = new byte[payload.position()]; 247 payload.position(0); 248 payload.get(payloadBytes); 249 return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_SELECT, null, 250 payloadBytes); 251 } 252 253 NdefRecord createHandoverRequestRecord() { 254 NdefMessage nestedMessage = new NdefMessage(createCollisionRecord(), 255 createBluetoothAlternateCarrierRecord(false)); 256 byte[] nestedPayload = nestedMessage.toByteArray(); 257 258 ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1); 259 payload.put((byte)0x12); // connection handover v1.2 260 payload.put(nestedMessage.toByteArray()); 261 262 byte[] payloadBytes = new byte[payload.position()]; 263 payload.position(0); 264 payload.get(payloadBytes); 265 return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, null, 266 payloadBytes); 267 } 268 269 /** 270 * Return null if message is not a Handover Request, 271 * return the Handover Select response if it is. 272 */ 273 public NdefMessage tryHandoverRequest(NdefMessage m) { 274 if (m == null) return null; 275 if (mBluetoothAdapter == null) return null; 276 277 if (DBG) Log.d(TAG, "tryHandoverRequest():" + m.toString()); 278 279 NdefRecord r = m.getRecords()[0]; 280 if (r.getTnf() != NdefRecord.TNF_WELL_KNOWN) return null; 281 if (!Arrays.equals(r.getType(), NdefRecord.RTD_HANDOVER_REQUEST)) return null; 282 283 // we have a handover request, look for BT OOB record 284 BluetoothHandoverData bluetoothData = null; 285 for (NdefRecord oob : m.getRecords()) { 286 if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA && 287 Arrays.equals(oob.getType(), TYPE_BT_OOB)) { 288 bluetoothData = parseBtOob(ByteBuffer.wrap(oob.getPayload())); 289 break; 290 } 291 } 292 if (bluetoothData == null) return null; 293 294 // Note: there could be a race where we conclude 295 // that Bluetooth is already enabled, and shortly 296 // after the user turns it off. That will cause 297 // the transfer to fail, but there's nothing 298 // much we can do about it anyway. It shouldn't 299 // be common for the user to be changing BT settings 300 // while waiting to receive a picture. 301 boolean bluetoothActivating = !mBluetoothAdapter.isEnabled(); 302 synchronized (mLock) { 303 if (!mEnabled) return null; 304 305 if (!mBound) { 306 Log.e(TAG, "Could not connect to handover service"); 307 return null; 308 } 309 Message msg = Message.obtain(null, HandoverService.MSG_START_INCOMING_TRANSFER); 310 PendingHandoverTransfer transfer = registerInTransferLocked(bluetoothData.device); 311 Bundle transferData = new Bundle(); 312 transferData.putParcelable(HandoverService.BUNDLE_TRANSFER, transfer); 313 msg.setData(transferData); 314 try { 315 mService.send(msg); 316 } catch (RemoteException e) { 317 Log.e(TAG, "Could not connect to handover service"); 318 removeTransferLocked(transfer.id); 319 return null; 320 } 321 } 322 // BT OOB found, whitelist it for incoming OPP data 323 whitelistOppDevice(bluetoothData.device); 324 325 // return BT OOB record so they can perform handover 326 NdefMessage selectMessage = (createHandoverSelectMessage(bluetoothActivating)); 327 if (DBG) Log.d(TAG, "Waiting for incoming transfer, [" + 328 bluetoothData.device.getAddress() + "]->[" + mLocalBluetoothAddress + "]"); 329 330 return selectMessage; 331 } 332 333 public boolean tryHandover(NdefMessage m) { 334 if (m == null) return false; 335 if (mBluetoothAdapter == null) return false; 336 337 if (DBG) Log.d(TAG, "tryHandover(): " + m.toString()); 338 339 BluetoothHandoverData handover = parse(m); 340 if (handover == null) return false; 341 if (!handover.valid) return true; 342 343 synchronized (mLock) { 344 if (!mEnabled) return false; 345 346 if (mBluetoothAdapter == null) { 347 if (DBG) Log.d(TAG, "BT handover, but BT not available"); 348 return true; 349 } 350 if (!mBound) { 351 Log.e(TAG, "Could not connect to handover service"); 352 return false; 353 } 354 355 Message msg = Message.obtain(null, HandoverService.MSG_HEADSET_HANDOVER, 0, 0); 356 Bundle headsetData = new Bundle(); 357 headsetData.putParcelable(HandoverService.EXTRA_HEADSET_DEVICE, handover.device); 358 headsetData.putString(HandoverService.EXTRA_HEADSET_NAME, handover.name); 359 msg.setData(headsetData); 360 try { 361 mService.send(msg); 362 } catch (RemoteException e) { 363 return false; 364 } 365 } 366 return true; 367 } 368 369 // This starts sending an Uri over BT 370 public void doHandoverUri(Uri[] uris, NdefMessage m) { 371 if (mBluetoothAdapter == null) return; 372 373 BluetoothHandoverData data = parse(m); 374 if (data != null && data.valid) { 375 // Register a new handover transfer object 376 synchronized (mLock) { 377 if (!mBound) { 378 Log.e(TAG, "Could not connect to handover service"); 379 return; 380 } 381 382 Message msg = Message.obtain(null, HandoverService.MSG_START_OUTGOING_TRANSFER, 0, 0); 383 PendingHandoverTransfer transfer = registerOutTransferLocked(data, uris); 384 Bundle transferData = new Bundle(); 385 transferData.putParcelable(HandoverService.BUNDLE_TRANSFER, transfer); 386 msg.setData(transferData); 387 if (DBG) Log.d(TAG, "Initiating outgoing transfer, [" + mLocalBluetoothAddress + 388 "]->[" + data.device.getAddress() + "]"); 389 try { 390 mService.send(msg); 391 } catch (RemoteException e) { 392 removeTransferLocked(transfer.id); 393 } 394 } 395 } 396 } 397 398 PendingHandoverTransfer registerInTransferLocked(BluetoothDevice remoteDevice) { 399 PendingHandoverTransfer transfer = new PendingHandoverTransfer( 400 mHandoverTransferId++, true, remoteDevice, false, null); 401 mPendingTransfers.put(transfer.id, transfer); 402 403 return transfer; 404 } 405 406 PendingHandoverTransfer registerOutTransferLocked(BluetoothHandoverData data, 407 Uri[] uris) { 408 PendingHandoverTransfer transfer = new PendingHandoverTransfer( 409 mHandoverTransferId++, false, data.device, data.carrierActivating, uris); 410 mPendingTransfers.put(transfer.id, transfer); 411 return transfer; 412 } 413 414 void removeTransferLocked(int id) { 415 mPendingTransfers.remove(id); 416 } 417 418 void whitelistOppDevice(BluetoothDevice device) { 419 if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP"); 420 Intent intent = new Intent(ACTION_WHITELIST_DEVICE); 421 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 422 mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); 423 } 424 425 boolean isCarrierActivating(NdefRecord handoverRec, byte[] carrierId) { 426 byte[] payload = handoverRec.getPayload(); 427 if (payload == null || payload.length <= 1) return false; 428 // Skip version 429 byte[] payloadNdef = new byte[payload.length - 1]; 430 System.arraycopy(payload, 1, payloadNdef, 0, payload.length - 1); 431 NdefMessage msg; 432 try { 433 msg = new NdefMessage(payloadNdef); 434 } catch (FormatException e) { 435 return false; 436 } 437 438 for (NdefRecord alt : msg.getRecords()) { 439 byte[] acPayload = alt.getPayload(); 440 if (acPayload != null) { 441 ByteBuffer buf = ByteBuffer.wrap(acPayload); 442 int cps = buf.get() & 0x03; // Carrier Power State is in lower 2 bits 443 int carrierRefLength = buf.get() & 0xFF; 444 if (carrierRefLength != carrierId.length) return false; 445 446 byte[] carrierRefId = new byte[carrierRefLength]; 447 buf.get(carrierRefId); 448 if (Arrays.equals(carrierRefId, carrierId)) { 449 // Found match, returning whether power state is activating 450 return (cps == CARRIER_POWER_STATE_ACTIVATING); 451 } 452 } 453 } 454 455 return true; 456 } 457 458 BluetoothHandoverData parseHandoverSelect(NdefMessage m) { 459 // TODO we could parse this a lot more strictly; right now 460 // we just search for a BT OOB record, and try to cross-reference 461 // the carrier state inside the 'hs' payload. 462 for (NdefRecord oob : m.getRecords()) { 463 if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA && 464 Arrays.equals(oob.getType(), TYPE_BT_OOB)) { 465 BluetoothHandoverData data = parseBtOob(ByteBuffer.wrap(oob.getPayload())); 466 if (data != null && isCarrierActivating(m.getRecords()[0], oob.getId())) { 467 data.carrierActivating = true; 468 } 469 return data; 470 } 471 } 472 473 return null; 474 } 475 476 BluetoothHandoverData parse(NdefMessage m) { 477 NdefRecord r = m.getRecords()[0]; 478 short tnf = r.getTnf(); 479 byte[] type = r.getType(); 480 481 // Check for BT OOB record 482 if (r.getTnf() == NdefRecord.TNF_MIME_MEDIA && Arrays.equals(r.getType(), TYPE_BT_OOB)) { 483 return parseBtOob(ByteBuffer.wrap(r.getPayload())); 484 } 485 486 // Check for Handover Select, followed by a BT OOB record 487 if (tnf == NdefRecord.TNF_WELL_KNOWN && 488 Arrays.equals(type, NdefRecord.RTD_HANDOVER_SELECT)) { 489 return parseHandoverSelect(m); 490 } 491 492 // Check for Nokia BT record, found on some Nokia BH-505 Headsets 493 if (tnf == NdefRecord.TNF_EXTERNAL_TYPE && Arrays.equals(type, TYPE_NOKIA)) { 494 return parseNokia(ByteBuffer.wrap(r.getPayload())); 495 } 496 497 return null; 498 } 499 500 BluetoothHandoverData parseNokia(ByteBuffer payload) { 501 BluetoothHandoverData result = new BluetoothHandoverData(); 502 result.valid = false; 503 504 try { 505 payload.position(1); 506 byte[] address = new byte[6]; 507 payload.get(address); 508 result.device = mBluetoothAdapter.getRemoteDevice(address); 509 result.valid = true; 510 payload.position(14); 511 int nameLength = payload.get(); 512 byte[] nameBytes = new byte[nameLength]; 513 payload.get(nameBytes); 514 result.name = new String(nameBytes, Charset.forName("UTF-8")); 515 } catch (IllegalArgumentException e) { 516 Log.i(TAG, "nokia: invalid BT address"); 517 } catch (BufferUnderflowException e) { 518 Log.i(TAG, "nokia: payload shorter than expected"); 519 } 520 if (result.valid && result.name == null) result.name = ""; 521 return result; 522 } 523 524 BluetoothHandoverData parseBtOob(ByteBuffer payload) { 525 BluetoothHandoverData result = new BluetoothHandoverData(); 526 result.valid = false; 527 528 try { 529 payload.position(2); 530 byte[] address = new byte[6]; 531 payload.get(address); 532 // ByteBuffer.order(LITTLE_ENDIAN) doesn't work for 533 // ByteBuffer.get(byte[]), so manually swap order 534 for (int i = 0; i < 3; i++) { 535 byte temp = address[i]; 536 address[i] = address[5 - i]; 537 address[5 - i] = temp; 538 } 539 result.device = mBluetoothAdapter.getRemoteDevice(address); 540 result.valid = true; 541 542 while (payload.remaining() > 0) { 543 byte[] nameBytes; 544 int len = payload.get(); 545 int type = payload.get(); 546 switch (type) { 547 case 0x08: // short local name 548 nameBytes = new byte[len - 1]; 549 payload.get(nameBytes); 550 result.name = new String(nameBytes, Charset.forName("UTF-8")); 551 break; 552 case 0x09: // long local name 553 if (result.name != null) break; // prefer short name 554 nameBytes = new byte[len - 1]; 555 payload.get(nameBytes); 556 result.name = new String(nameBytes, Charset.forName("UTF-8")); 557 break; 558 default: 559 payload.position(payload.position() + len - 1); 560 break; 561 } 562 } 563 } catch (IllegalArgumentException e) { 564 Log.i(TAG, "BT OOB: invalid BT address"); 565 } catch (BufferUnderflowException e) { 566 Log.i(TAG, "BT OOB: payload shorter than expected"); 567 } 568 if (result.valid && result.name == null) result.name = ""; 569 return result; 570 } 571 572 static byte[] addressToReverseBytes(String address) { 573 String[] split = address.split(":"); 574 byte[] result = new byte[split.length]; 575 576 for (int i = 0; i < split.length; i++) { 577 // need to parse as int because parseByte() expects a signed byte 578 result[split.length - 1 - i] = (byte)Integer.parseInt(split[i], 16); 579 } 580 581 return result; 582 } 583 } 584