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