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.io.File; 20 import java.nio.BufferUnderflowException; 21 import java.nio.ByteBuffer; 22 import java.nio.charset.Charset; 23 import java.text.SimpleDateFormat; 24 import java.util.ArrayList; 25 import java.util.Arrays; 26 import java.util.Date; 27 import java.util.HashMap; 28 import java.util.Iterator; 29 import java.util.Map; 30 import java.util.Random; 31 32 import android.app.Notification; 33 import android.app.NotificationManager; 34 import android.app.PendingIntent; 35 import android.app.Notification.Builder; 36 import android.bluetooth.BluetoothA2dp; 37 import android.bluetooth.BluetoothAdapter; 38 import android.bluetooth.BluetoothDevice; 39 import android.bluetooth.BluetoothHeadset; 40 import android.bluetooth.BluetoothProfile; 41 import android.content.BroadcastReceiver; 42 import android.content.ContentResolver; 43 import android.content.Context; 44 import android.content.Intent; 45 import android.content.IntentFilter; 46 import android.media.MediaScannerConnection; 47 import android.net.Uri; 48 import android.nfc.FormatException; 49 import android.nfc.NdefMessage; 50 import android.nfc.NdefRecord; 51 import android.os.Environment; 52 import android.os.Handler; 53 import android.os.Message; 54 import android.os.SystemClock; 55 import android.util.Log; 56 import android.util.Pair; 57 58 import com.android.nfc.NfcService; 59 import com.android.nfc.R; 60 61 62 /** 63 * Manages handover of NFC to other technologies. 64 */ 65 public class HandoverManager implements BluetoothProfile.ServiceListener, 66 BluetoothHeadsetHandover.Callback { 67 static final String TAG = "NfcHandover"; 68 static final boolean DBG = true; 69 70 static final byte[] TYPE_NOKIA = "nokia.com:bt".getBytes(Charset.forName("US_ASCII")); 71 static final byte[] TYPE_BT_OOB = "application/vnd.bluetooth.ep.oob". 72 getBytes(Charset.forName("US_ASCII")); 73 74 static final String ACTION_BT_OPP_TRANSFER_PROGRESS = 75 "android.btopp.intent.action.BT_OPP_TRANSFER_PROGRESS"; 76 77 static final String ACTION_BT_OPP_TRANSFER_DONE = 78 "android.btopp.intent.action.BT_OPP_TRANSFER_DONE"; 79 80 static final String EXTRA_BT_OPP_TRANSFER_STATUS = 81 "android.btopp.intent.extra.BT_OPP_TRANSFER_STATUS"; 82 83 static final String EXTRA_BT_OPP_TRANSFER_MIMETYPE = 84 "android.btopp.intent.extra.BT_OPP_TRANSFER_MIMETYPE"; 85 86 static final String EXTRA_BT_OPP_ADDRESS = 87 "android.btopp.intent.extra.BT_OPP_ADDRESS"; 88 89 static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0; 90 91 static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1; 92 93 static final String EXTRA_BT_OPP_TRANSFER_DIRECTION = 94 "android.btopp.intent.extra.BT_OPP_TRANSFER_DIRECTION"; 95 96 static final int DIRECTION_BLUETOOTH_INCOMING = 0; 97 98 static final int DIRECTION_BLUETOOTH_OUTGOING = 1; 99 100 static final String EXTRA_BT_OPP_TRANSFER_ID = 101 "android.btopp.intent.extra.BT_OPP_TRANSFER_ID"; 102 103 static final String EXTRA_BT_OPP_TRANSFER_PROGRESS = 104 "android.btopp.intent.extra.BT_OPP_TRANSFER_PROGRESS"; 105 106 static final String EXTRA_BT_OPP_TRANSFER_URI = 107 "android.btopp.intent.extra.BT_OPP_TRANSFER_URI"; 108 109 // permission needed to be able to receive handover status requests 110 static final String HANDOVER_STATUS_PERMISSION = 111 "com.android.permission.HANDOVER_STATUS"; 112 113 static final int MSG_HANDOVER_POWER_CHECK = 0; 114 115 // We poll whether we can safely disable BT every POWER_CHECK_MS 116 static final int POWER_CHECK_MS = 20000; 117 118 static final String ACTION_WHITELIST_DEVICE = 119 "android.btopp.intent.action.WHITELIST_DEVICE"; 120 121 static final String ACTION_CANCEL_HANDOVER_TRANSFER = 122 "com.android.nfc.handover.action.CANCEL_HANDOVER_TRANSFER"; 123 static final String EXTRA_SOURCE_ADDRESS = 124 "com.android.nfc.handover.extra.SOURCE_ADDRESS"; 125 126 static final int SOURCE_BLUETOOTH_INCOMING = 0; 127 128 static final int SOURCE_BLUETOOTH_OUTGOING = 1; 129 130 static final int CARRIER_POWER_STATE_INACTIVE = 0; 131 static final int CARRIER_POWER_STATE_ACTIVE = 1; 132 static final int CARRIER_POWER_STATE_ACTIVATING = 2; 133 static final int CARRIER_POWER_STATE_UNKNOWN = 3; 134 135 final Context mContext; 136 final BluetoothAdapter mBluetoothAdapter; 137 final NotificationManager mNotificationManager; 138 final HandoverPowerManager mHandoverPowerManager; 139 140 // Variables below synchronized on HandoverManager.this 141 final HashMap<Pair<String, Boolean>, HandoverTransfer> mTransfers; 142 143 BluetoothHeadset mBluetoothHeadset; 144 BluetoothA2dp mBluetoothA2dp; 145 BluetoothHeadsetHandover mBluetoothHeadsetHandover; 146 boolean mBluetoothHeadsetConnected; 147 148 String mLocalBluetoothAddress; 149 int mNotificationId; 150 151 static class BluetoothHandoverData { 152 public boolean valid = false; 153 public BluetoothDevice device; 154 public String name; 155 public boolean carrierActivating = false; 156 } 157 158 class HandoverPowerManager implements Handler.Callback { 159 final Handler handler; 160 final Context context; 161 162 public HandoverPowerManager(Context context) { 163 this.handler = new Handler(this); 164 this.context = context; 165 } 166 167 /** 168 * Enables Bluetooth and will automatically disable it 169 * when there is no Bluetooth activity intitiated by NFC 170 * anymore. 171 */ 172 synchronized boolean enableBluetooth() { 173 // Enable BT 174 boolean result = mBluetoothAdapter.enableNoAutoConnect(); 175 176 if (result) { 177 // Start polling for BT activity to make sure we eventually disable 178 // it again. 179 handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS); 180 } 181 return result; 182 } 183 184 synchronized boolean isBluetoothEnabled() { 185 return mBluetoothAdapter.isEnabled(); 186 } 187 188 synchronized void resetTimer() { 189 if (handler.hasMessages(MSG_HANDOVER_POWER_CHECK)) { 190 handler.removeMessages(MSG_HANDOVER_POWER_CHECK); 191 handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS); 192 } 193 } 194 195 void stopMonitoring() { 196 handler.removeMessages(MSG_HANDOVER_POWER_CHECK); 197 } 198 199 @Override 200 public boolean handleMessage(Message msg) { 201 switch (msg.what) { 202 case MSG_HANDOVER_POWER_CHECK: 203 // Check for any alive transfers 204 boolean transferAlive = false; 205 synchronized (HandoverManager.this) { 206 for (HandoverTransfer transfer : mTransfers.values()) { 207 if (transfer.isRunning()) { 208 transferAlive = true; 209 } 210 } 211 212 if (!transferAlive && !mBluetoothHeadsetConnected) { 213 mBluetoothAdapter.disable(); 214 handler.removeMessages(MSG_HANDOVER_POWER_CHECK); 215 } else { 216 handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS); 217 } 218 } 219 return true; 220 } 221 return false; 222 } 223 } 224 225 /** 226 * A HandoverTransfer object represents a set of files 227 * that were received through NFC connection handover 228 * from the same source address. 229 * 230 * For Bluetooth, files are received through OPP, and 231 * we have no knowledge how many files will be transferred 232 * as part of a single transaction. 233 * Hence, a transfer has a notion of being "alive": if 234 * the last update to a transfer was within WAIT_FOR_NEXT_TRANSFER_MS 235 * milliseconds, we consider a new file transfer from the 236 * same source address as part of the same transfer. 237 * The corresponding URIs will be grouped in a single folder. 238 * 239 */ 240 class HandoverTransfer implements Handler.Callback, 241 MediaScannerConnection.OnScanCompletedListener { 242 // In the states below we still accept new file transfer 243 static final int STATE_NEW = 0; 244 static final int STATE_IN_PROGRESS = 1; 245 static final int STATE_W4_NEXT_TRANSFER = 2; 246 247 // In the states below no new files are accepted. 248 static final int STATE_W4_MEDIA_SCANNER = 3; 249 static final int STATE_FAILED = 4; 250 static final int STATE_SUCCESS = 5; 251 static final int STATE_CANCELLED = 6; 252 253 static final int MSG_NEXT_TRANSFER_TIMER = 0; 254 static final int MSG_TRANSFER_TIMEOUT = 1; 255 256 // We need to receive an update within this time period 257 // to still consider this transfer to be "alive" (ie 258 // a reason to keep the handover transport enabled). 259 static final int ALIVE_CHECK_MS = 20000; 260 261 // The amount of time to wait for a new transfer 262 // once the current one completes. 263 static final int WAIT_FOR_NEXT_TRANSFER_MS = 4000; 264 265 static final String BEAM_DIR = "beam"; 266 267 final BluetoothDevice device; 268 final String sourceAddress; 269 final boolean incoming; // whether this is an incoming transfer 270 final int notificationId; // Unique ID of this transfer used for notifications 271 final Handler handler; 272 final PendingIntent cancelIntent; 273 274 int state; 275 Long lastUpdate; // Last time an event occurred for this transfer 276 float progress; // Progress in range [0..1] 277 ArrayList<Uri> btUris; // Received uris from Bluetooth OPP 278 ArrayList<String> btMimeTypes; // Mime-types received from Bluetooth OPP 279 280 ArrayList<String> paths; // Raw paths on the filesystem for Beam-stored files 281 HashMap<String, String> mimeTypes; // Mime-types associated with each path 282 HashMap<String, Uri> mediaUris; // URIs found by the media scanner for each path 283 int urisScanned; 284 285 public HandoverTransfer(String sourceAddress, boolean incoming) { 286 synchronized (HandoverManager.this) { 287 this.notificationId = mNotificationId++; 288 } 289 this.lastUpdate = SystemClock.elapsedRealtime(); 290 this.progress = 0.0f; 291 this.state = STATE_NEW; 292 this.btUris = new ArrayList<Uri>(); 293 this.btMimeTypes = new ArrayList<String>(); 294 this.paths = new ArrayList<String>(); 295 this.mimeTypes = new HashMap<String, String>(); 296 this.mediaUris = new HashMap<String, Uri>(); 297 this.sourceAddress = sourceAddress; 298 this.incoming = incoming; 299 this.handler = new Handler(mContext.getMainLooper(), this); 300 this.cancelIntent = buildCancelIntent(); 301 this.urisScanned = 0; 302 this.device = mBluetoothAdapter.getRemoteDevice(sourceAddress); 303 304 handler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS); 305 } 306 307 public synchronized void updateFileProgress(float progress) { 308 if (!isRunning()) return; // Ignore when we're no longer running 309 310 handler.removeMessages(MSG_NEXT_TRANSFER_TIMER); 311 312 this.progress = progress; 313 314 // We're still receiving data from this device - keep it in 315 // the whitelist for a while longer 316 if (incoming) whitelistOppDevice(device); 317 318 updateStateAndNotification(STATE_IN_PROGRESS); 319 } 320 321 public synchronized void finishTransfer(boolean success, Uri uri, String mimeType) { 322 if (!isRunning()) return; // Ignore when we're no longer running 323 324 if (success && uri != null) { 325 if (DBG) Log.d(TAG, "Transfer success, uri " + uri + " mimeType " + mimeType); 326 this.progress = 1.0f; 327 if (mimeType == null) { 328 mimeType = BluetoothOppHandover.getMimeTypeForUri(mContext, uri); 329 } 330 if (mimeType != null) { 331 btUris.add(uri); 332 btMimeTypes.add(mimeType); 333 } else { 334 if (DBG) Log.d(TAG, "Could not get mimeType for file."); 335 } 336 } else { 337 Log.e(TAG, "Handover transfer failed"); 338 // Do wait to see if there's another file coming. 339 } 340 handler.removeMessages(MSG_NEXT_TRANSFER_TIMER); 341 handler.sendEmptyMessageDelayed(MSG_NEXT_TRANSFER_TIMER, WAIT_FOR_NEXT_TRANSFER_MS); 342 updateStateAndNotification(STATE_W4_NEXT_TRANSFER); 343 } 344 345 public synchronized boolean isRunning() { 346 if (state != STATE_NEW && state != STATE_IN_PROGRESS && state != STATE_W4_NEXT_TRANSFER) { 347 return false; 348 } else { 349 return true; 350 } 351 } 352 353 synchronized void cancel() { 354 if (!isRunning()) return; 355 356 // Delete all files received so far 357 for (Uri uri : btUris) { 358 File file = new File(uri.getPath()); 359 if (file.exists()) file.delete(); 360 } 361 362 updateStateAndNotification(STATE_CANCELLED); 363 } 364 365 synchronized void updateNotification() { 366 if (!incoming) return; // No notifications for outgoing transfers 367 368 Builder notBuilder = new Notification.Builder(mContext); 369 370 if (state == STATE_NEW || state == STATE_IN_PROGRESS || 371 state == STATE_W4_NEXT_TRANSFER || state == STATE_W4_MEDIA_SCANNER) { 372 notBuilder.setAutoCancel(false); 373 notBuilder.setSmallIcon(android.R.drawable.stat_sys_download); 374 notBuilder.setTicker(mContext.getString(R.string.beam_progress)); 375 notBuilder.setContentTitle(mContext.getString(R.string.beam_progress)); 376 notBuilder.addAction(R.drawable.ic_menu_cancel_holo_dark, 377 mContext.getString(R.string.cancel), cancelIntent); 378 notBuilder.setDeleteIntent(cancelIntent); 379 // We do have progress indication on a per-file basis, but in a multi-file 380 // transfer we don't know the total progress. So for now, just show an 381 // indeterminate progress bar. 382 notBuilder.setProgress(100, 0, true); 383 } else if (state == STATE_SUCCESS) { 384 notBuilder.setAutoCancel(true); 385 notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done); 386 notBuilder.setTicker(mContext.getString(R.string.beam_complete)); 387 notBuilder.setContentTitle(mContext.getString(R.string.beam_complete)); 388 notBuilder.setContentText(mContext.getString(R.string.beam_touch_to_view)); 389 390 Intent viewIntent = buildViewIntent(); 391 PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, viewIntent, 0); 392 393 notBuilder.setContentIntent(contentIntent); 394 395 // Play Beam success sound 396 NfcService.getInstance().playSound(NfcService.SOUND_END); 397 } else if (state == STATE_FAILED) { 398 notBuilder.setAutoCancel(false); 399 notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done); 400 notBuilder.setTicker(mContext.getString(R.string.beam_failed)); 401 notBuilder.setContentTitle(mContext.getString(R.string.beam_failed)); 402 } else if (state == STATE_CANCELLED) { 403 notBuilder.setAutoCancel(false); 404 notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done); 405 notBuilder.setTicker(mContext.getString(R.string.beam_canceled)); 406 notBuilder.setContentTitle(mContext.getString(R.string.beam_canceled)); 407 } else { 408 return; 409 } 410 411 mNotificationManager.notify(mNotificationId, notBuilder.build()); 412 } 413 414 synchronized void updateStateAndNotification(int newState) { 415 this.state = newState; 416 this.lastUpdate = SystemClock.elapsedRealtime(); 417 418 if (handler.hasMessages(MSG_TRANSFER_TIMEOUT)) { 419 // Update timeout timer 420 handler.removeMessages(MSG_TRANSFER_TIMEOUT); 421 handler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS); 422 } 423 updateNotification(); 424 } 425 426 synchronized void processFiles() { 427 // Check the amount of files we received in this transfer; 428 // If more than one, create a separate directory for it. 429 String extRoot = Environment.getExternalStorageDirectory().getPath(); 430 File beamPath = new File(extRoot + "/" + BEAM_DIR); 431 432 if (!checkMediaStorage(beamPath) || btUris.size() == 0) { 433 Log.e(TAG, "Media storage not valid or no uris received."); 434 updateStateAndNotification(STATE_FAILED); 435 return; 436 } 437 438 if (btUris.size() > 1) { 439 beamPath = generateMultiplePath(extRoot + "/" + BEAM_DIR + "/"); 440 if (!beamPath.isDirectory() && !beamPath.mkdir()) { 441 Log.e(TAG, "Failed to create multiple path " + beamPath.toString()); 442 updateStateAndNotification(STATE_FAILED); 443 return; 444 } 445 } 446 447 for (int i = 0; i < btUris.size(); i++) { 448 Uri uri = btUris.get(i); 449 String mimeType = btMimeTypes.get(i); 450 451 File srcFile = new File(uri.getPath()); 452 453 File dstFile = generateUniqueDestination(beamPath.getAbsolutePath(), 454 uri.getLastPathSegment()); 455 if (!srcFile.renameTo(dstFile)) { 456 if (DBG) Log.d(TAG, "Failed to rename from " + srcFile + " to " + dstFile); 457 srcFile.delete(); 458 return; 459 } else { 460 paths.add(dstFile.getAbsolutePath()); 461 mimeTypes.put(dstFile.getAbsolutePath(), mimeType); 462 if (DBG) Log.d(TAG, "Did successful rename from " + srcFile + " to " + dstFile); 463 } 464 } 465 466 // We can either add files to the media provider, or provide an ACTION_VIEW 467 // intent to the file directly. We base this decision on the mime type 468 // of the first file; if it's media the platform can deal with, 469 // use the media provider, if it's something else, just launch an ACTION_VIEW 470 // on the file. 471 String mimeType = mimeTypes.get(paths.get(0)); 472 if (mimeType.startsWith("image/") || mimeType.startsWith("video/") || 473 mimeType.startsWith("audio/")) { 474 String[] arrayPaths = new String[paths.size()]; 475 MediaScannerConnection.scanFile(mContext, paths.toArray(arrayPaths), null, this); 476 updateStateAndNotification(STATE_W4_MEDIA_SCANNER); 477 } else { 478 // We're done. 479 updateStateAndNotification(STATE_SUCCESS); 480 } 481 482 } 483 484 public boolean handleMessage(Message msg) { 485 if (msg.what == MSG_NEXT_TRANSFER_TIMER) { 486 // We didn't receive a new transfer in time, finalize this one 487 if (incoming) { 488 processFiles(); 489 } else { 490 updateStateAndNotification(STATE_SUCCESS); 491 } 492 return true; 493 } else if (msg.what == MSG_TRANSFER_TIMEOUT) { 494 // No update on this transfer for a while, check 495 // to see if it's still running, and fail it if it is. 496 if (isRunning()) { 497 updateStateAndNotification(STATE_FAILED); 498 } 499 } 500 return false; 501 } 502 503 public synchronized void onScanCompleted(String path, Uri uri) { 504 if (DBG) Log.d(TAG, "Scan completed, path " + path + " uri " + uri); 505 if (uri != null) { 506 mediaUris.put(path, uri); 507 } 508 urisScanned++; 509 if (urisScanned == paths.size()) { 510 // We're done 511 updateStateAndNotification(STATE_SUCCESS); 512 } 513 } 514 515 boolean checkMediaStorage(File path) { 516 if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 517 if (!path.isDirectory() && !path.mkdir()) { 518 Log.e(TAG, "Not dir or not mkdir " + path.getAbsolutePath()); 519 return false; 520 } 521 return true; 522 } else { 523 Log.e(TAG, "External storage not mounted, can't store file."); 524 return false; 525 } 526 } 527 528 synchronized Intent buildViewIntent() { 529 if (paths.size() == 0) return null; 530 531 Intent viewIntent = new Intent(Intent.ACTION_VIEW); 532 533 String filePath = paths.get(0); 534 Uri mediaUri = mediaUris.get(filePath); 535 Uri uri = mediaUri != null ? mediaUri : 536 Uri.parse(ContentResolver.SCHEME_FILE + "://" + filePath); 537 viewIntent.setDataAndTypeAndNormalize(uri, mimeTypes.get(filePath)); 538 539 return viewIntent; 540 } 541 542 PendingIntent buildCancelIntent() { 543 Intent intent = new Intent(ACTION_CANCEL_HANDOVER_TRANSFER); 544 intent.putExtra(EXTRA_SOURCE_ADDRESS, sourceAddress); 545 PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0); 546 547 return pi; 548 } 549 550 synchronized File generateUniqueDestination(String path, String fileName) { 551 int dotIndex = fileName.lastIndexOf("."); 552 String extension = null; 553 String fileNameWithoutExtension = null; 554 if (dotIndex < 0) { 555 extension = ""; 556 fileNameWithoutExtension = fileName; 557 } else { 558 extension = fileName.substring(dotIndex); 559 fileNameWithoutExtension = fileName.substring(0, dotIndex); 560 } 561 File dstFile = new File(path + File.separator + fileName); 562 int count = 0; 563 while (dstFile.exists()) { 564 dstFile = new File(path + File.separator + fileNameWithoutExtension + "-" + 565 Integer.toString(count) + extension); 566 count++; 567 } 568 return dstFile; 569 } 570 571 synchronized File generateMultiplePath(String beamRoot) { 572 // Generate a unique directory with the date 573 String format = "yyyy-MM-dd"; 574 SimpleDateFormat sdf = new SimpleDateFormat(format); 575 String newPath = beamRoot + "beam-" + sdf.format(new Date()); 576 File newFile = new File(newPath); 577 int count = 0; 578 while (newFile.exists()) { 579 newPath = beamRoot + "beam-" + sdf.format(new Date()) + "-" + 580 Integer.toString(count); 581 newFile = new File(newPath); 582 count++; 583 } 584 585 return newFile; 586 } 587 } 588 589 synchronized HandoverTransfer getOrCreateHandoverTransfer(String sourceAddress, boolean incoming, 590 boolean create) { 591 Pair<String, Boolean> key = new Pair<String, Boolean>(sourceAddress, incoming); 592 if (mTransfers.containsKey(key)) { 593 HandoverTransfer transfer = mTransfers.get(key); 594 if (transfer.isRunning()) { 595 return transfer; 596 } else { 597 if (create) mTransfers.remove(key); // new one created below 598 } 599 } 600 if (create) { 601 HandoverTransfer transfer = new HandoverTransfer(sourceAddress, incoming); 602 mTransfers.put(key, transfer); 603 604 return transfer; 605 } else { 606 return null; 607 } 608 } 609 610 public HandoverManager(Context context) { 611 mContext = context; 612 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 613 if (mBluetoothAdapter != null) { 614 mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET); 615 mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP); 616 } 617 618 mNotificationManager = (NotificationManager) mContext.getSystemService( 619 Context.NOTIFICATION_SERVICE); 620 621 mTransfers = new HashMap<Pair<String, Boolean>, HandoverTransfer>(); 622 mHandoverPowerManager = new HandoverPowerManager(context); 623 624 IntentFilter filter = new IntentFilter(ACTION_BT_OPP_TRANSFER_DONE); 625 filter.addAction(ACTION_BT_OPP_TRANSFER_PROGRESS); 626 filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 627 filter.addAction(ACTION_CANCEL_HANDOVER_TRANSFER); 628 mContext.registerReceiver(mReceiver, filter, HANDOVER_STATUS_PERMISSION, null); 629 } 630 631 synchronized void cleanupTransfers() { 632 Iterator<Map.Entry<Pair<String, Boolean>, HandoverTransfer>> it = mTransfers.entrySet().iterator(); 633 while (it.hasNext()) { 634 Map.Entry<Pair<String, Boolean>, HandoverTransfer> pair = it.next(); 635 HandoverTransfer transfer = pair.getValue(); 636 if (!transfer.isRunning()) { 637 it.remove(); 638 } 639 } 640 } 641 642 static NdefRecord createCollisionRecord() { 643 byte[] random = new byte[2]; 644 new Random().nextBytes(random); 645 return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, null, random); 646 } 647 648 NdefRecord createBluetoothAlternateCarrierRecord(boolean activating) { 649 byte[] payload = new byte[4]; 650 payload[0] = (byte) (activating ? CARRIER_POWER_STATE_ACTIVATING : 651 CARRIER_POWER_STATE_ACTIVE); // Carrier Power State: Activating or active 652 payload[1] = 1; // length of carrier data reference 653 payload[2] = 'b'; // carrier data reference: ID for Bluetooth OOB data record 654 payload[3] = 0; // Auxiliary data reference count 655 return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_ALTERNATIVE_CARRIER, null, payload); 656 } 657 658 NdefRecord createBluetoothOobDataRecord() { 659 byte[] payload = new byte[8]; 660 payload[0] = 0; 661 payload[1] = (byte)payload.length; 662 663 synchronized (HandoverManager.this) { 664 if (mLocalBluetoothAddress == null) { 665 mLocalBluetoothAddress = mBluetoothAdapter.getAddress(); 666 } 667 668 byte[] addressBytes = addressToReverseBytes(mLocalBluetoothAddress); 669 System.arraycopy(addressBytes, 0, payload, 2, 6); 670 } 671 672 return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, TYPE_BT_OOB, new byte[]{'b'}, payload); 673 } 674 675 public boolean isHandoverSupported() { 676 return (mBluetoothAdapter != null); 677 } 678 679 public NdefMessage createHandoverRequestMessage() { 680 if (mBluetoothAdapter == null) return null; 681 682 return new NdefMessage(createHandoverRequestRecord(), createBluetoothOobDataRecord()); 683 } 684 685 NdefMessage createHandoverSelectMessage(boolean activating) { 686 return new NdefMessage(createHandoverSelectRecord(activating), createBluetoothOobDataRecord()); 687 } 688 689 NdefRecord createHandoverSelectRecord(boolean activating) { 690 NdefMessage nestedMessage = new NdefMessage(createBluetoothAlternateCarrierRecord(activating)); 691 byte[] nestedPayload = nestedMessage.toByteArray(); 692 693 ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1); 694 payload.put((byte)0x12); // connection handover v1.2 695 payload.put(nestedPayload); 696 697 byte[] payloadBytes = new byte[payload.position()]; 698 payload.position(0); 699 payload.get(payloadBytes); 700 return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_SELECT, null, 701 payloadBytes); 702 703 } 704 705 NdefRecord createHandoverRequestRecord() { 706 NdefMessage nestedMessage = new NdefMessage(createCollisionRecord(), 707 createBluetoothAlternateCarrierRecord(false)); 708 byte[] nestedPayload = nestedMessage.toByteArray(); 709 710 ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1); 711 payload.put((byte)0x12); // connection handover v1.2 712 payload.put(nestedMessage.toByteArray()); 713 714 byte[] payloadBytes = new byte[payload.position()]; 715 payload.position(0); 716 payload.get(payloadBytes); 717 return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, null, 718 payloadBytes); 719 } 720 721 /** 722 * Return null if message is not a Handover Request, 723 * return the Handover Select response if it is. 724 */ 725 public NdefMessage tryHandoverRequest(NdefMessage m) { 726 if (m == null) return null; 727 if (mBluetoothAdapter == null) return null; 728 729 if (DBG) Log.d(TAG, "tryHandoverRequest():" + m.toString()); 730 731 NdefRecord r = m.getRecords()[0]; 732 if (r.getTnf() != NdefRecord.TNF_WELL_KNOWN) return null; 733 if (!Arrays.equals(r.getType(), NdefRecord.RTD_HANDOVER_REQUEST)) return null; 734 735 // we have a handover request, look for BT OOB record 736 BluetoothHandoverData bluetoothData = null; 737 for (NdefRecord oob : m.getRecords()) { 738 if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA && 739 Arrays.equals(oob.getType(), TYPE_BT_OOB)) { 740 bluetoothData = parseBtOob(ByteBuffer.wrap(oob.getPayload())); 741 break; 742 } 743 } 744 if (bluetoothData == null) return null; 745 746 boolean bluetoothActivating = false; 747 748 synchronized(HandoverManager.this) { 749 if (!mHandoverPowerManager.isBluetoothEnabled()) { 750 if (!mHandoverPowerManager.enableBluetooth()) { 751 return null; 752 } 753 bluetoothActivating = true; 754 } else { 755 mHandoverPowerManager.resetTimer(); 756 } 757 758 // Create the initial transfer object 759 HandoverTransfer transfer = getOrCreateHandoverTransfer( 760 bluetoothData.device.getAddress(), true, true); 761 transfer.updateNotification(); 762 } 763 764 // BT OOB found, whitelist it for incoming OPP data 765 whitelistOppDevice(bluetoothData.device); 766 767 // return BT OOB record so they can perform handover 768 return (createHandoverSelectMessage(bluetoothActivating)); 769 } 770 771 void whitelistOppDevice(BluetoothDevice device) { 772 if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP"); 773 Intent intent = new Intent(ACTION_WHITELIST_DEVICE); 774 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 775 mContext.sendBroadcast(intent); 776 } 777 778 public boolean tryHandover(NdefMessage m) { 779 if (m == null) return false; 780 if (mBluetoothAdapter == null) return false; 781 782 if (DBG) Log.d(TAG, "tryHandover(): " + m.toString()); 783 784 BluetoothHandoverData handover = parse(m); 785 if (handover == null) return false; 786 if (!handover.valid) return true; 787 788 synchronized (HandoverManager.this) { 789 if (mBluetoothAdapter == null || 790 mBluetoothA2dp == null || 791 mBluetoothHeadset == null) { 792 if (DBG) Log.d(TAG, "BT handover, but BT not available"); 793 return true; 794 } 795 if (mBluetoothHeadsetHandover != null) { 796 if (DBG) Log.d(TAG, "BT handover already in progress, ignoring"); 797 return true; 798 } 799 mBluetoothHeadsetHandover = new BluetoothHeadsetHandover(mContext, handover.device, 800 handover.name, mHandoverPowerManager, mBluetoothA2dp, mBluetoothHeadset, this); 801 mBluetoothHeadsetHandover.start(); 802 } 803 return true; 804 } 805 806 // This starts sending an Uri over BT 807 public void doHandoverUri(Uri[] uris, NdefMessage m) { 808 if (mBluetoothAdapter == null) return; 809 810 BluetoothHandoverData data = parse(m); 811 if (data != null && data.valid) { 812 // Register a new handover transfer object 813 getOrCreateHandoverTransfer(data.device.getAddress(), false, true); 814 BluetoothOppHandover handover = new BluetoothOppHandover(mContext, data.device, 815 uris, mHandoverPowerManager, data.carrierActivating); 816 handover.start(); 817 } 818 } 819 820 boolean isCarrierActivating(NdefRecord handoverRec, byte[] carrierId) { 821 byte[] payload = handoverRec.getPayload(); 822 if (payload == null || payload.length <= 1) return false; 823 // Skip version 824 byte[] payloadNdef = new byte[payload.length - 1]; 825 System.arraycopy(payload, 1, payloadNdef, 0, payload.length - 1); 826 NdefMessage msg; 827 try { 828 msg = new NdefMessage(payloadNdef); 829 } catch (FormatException e) { 830 return false; 831 } 832 833 for (NdefRecord alt : msg.getRecords()) { 834 byte[] acPayload = alt.getPayload(); 835 if (acPayload != null) { 836 ByteBuffer buf = ByteBuffer.wrap(acPayload); 837 int cps = buf.get() & 0x03; // Carrier Power State is in lower 2 bits 838 int carrierRefLength = buf.get() & 0xFF; 839 if (carrierRefLength != carrierId.length) return false; 840 841 byte[] carrierRefId = new byte[carrierRefLength]; 842 buf.get(carrierRefId); 843 if (Arrays.equals(carrierRefId, carrierId)) { 844 // Found match, returning whether power state is activating 845 return (cps == CARRIER_POWER_STATE_ACTIVATING); 846 } 847 } 848 } 849 850 return true; 851 } 852 853 BluetoothHandoverData parseHandoverSelect(NdefMessage m) { 854 // TODO we could parse this a lot more strictly; right now 855 // we just search for a BT OOB record, and try to cross-reference 856 // the carrier state inside the 'hs' payload. 857 for (NdefRecord oob : m.getRecords()) { 858 if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA && 859 Arrays.equals(oob.getType(), TYPE_BT_OOB)) { 860 BluetoothHandoverData data = parseBtOob(ByteBuffer.wrap(oob.getPayload())); 861 if (data != null && isCarrierActivating(m.getRecords()[0], oob.getId())) { 862 data.carrierActivating = true; 863 } 864 return data; 865 } 866 } 867 868 return null; 869 } 870 871 BluetoothHandoverData parse(NdefMessage m) { 872 NdefRecord r = m.getRecords()[0]; 873 short tnf = r.getTnf(); 874 byte[] type = r.getType(); 875 876 // Check for BT OOB record 877 if (r.getTnf() == NdefRecord.TNF_MIME_MEDIA && Arrays.equals(r.getType(), TYPE_BT_OOB)) { 878 return parseBtOob(ByteBuffer.wrap(r.getPayload())); 879 } 880 881 // Check for Handover Select, followed by a BT OOB record 882 if (tnf == NdefRecord.TNF_WELL_KNOWN && 883 Arrays.equals(type, NdefRecord.RTD_HANDOVER_SELECT)) { 884 return parseHandoverSelect(m); 885 } 886 887 // Check for Nokia BT record, found on some Nokia BH-505 Headsets 888 if (tnf == NdefRecord.TNF_EXTERNAL_TYPE && Arrays.equals(type, TYPE_NOKIA)) { 889 return parseNokia(ByteBuffer.wrap(r.getPayload())); 890 } 891 892 return null; 893 } 894 895 BluetoothHandoverData parseNokia(ByteBuffer payload) { 896 BluetoothHandoverData result = new BluetoothHandoverData(); 897 result.valid = false; 898 899 try { 900 payload.position(1); 901 byte[] address = new byte[6]; 902 payload.get(address); 903 result.device = mBluetoothAdapter.getRemoteDevice(address); 904 result.valid = true; 905 payload.position(14); 906 int nameLength = payload.get(); 907 byte[] nameBytes = new byte[nameLength]; 908 payload.get(nameBytes); 909 result.name = new String(nameBytes, Charset.forName("UTF-8")); 910 } catch (IllegalArgumentException e) { 911 Log.i(TAG, "nokia: invalid BT address"); 912 } catch (BufferUnderflowException e) { 913 Log.i(TAG, "nokia: payload shorter than expected"); 914 } 915 if (result.valid && result.name == null) result.name = ""; 916 return result; 917 } 918 919 BluetoothHandoverData parseBtOob(ByteBuffer payload) { 920 BluetoothHandoverData result = new BluetoothHandoverData(); 921 result.valid = false; 922 923 try { 924 payload.position(2); 925 byte[] address = new byte[6]; 926 payload.get(address); 927 // ByteBuffer.order(LITTLE_ENDIAN) doesn't work for 928 // ByteBuffer.get(byte[]), so manually swap order 929 for (int i = 0; i < 3; i++) { 930 byte temp = address[i]; 931 address[i] = address[5 - i]; 932 address[5 - i] = temp; 933 } 934 result.device = mBluetoothAdapter.getRemoteDevice(address); 935 result.valid = true; 936 937 while (payload.remaining() > 0) { 938 byte[] nameBytes; 939 int len = payload.get(); 940 int type = payload.get(); 941 switch (type) { 942 case 0x08: // short local name 943 nameBytes = new byte[len - 1]; 944 payload.get(nameBytes); 945 result.name = new String(nameBytes, Charset.forName("UTF-8")); 946 break; 947 case 0x09: // long local name 948 if (result.name != null) break; // prefer short name 949 nameBytes = new byte[len - 1]; 950 payload.get(nameBytes); 951 result.name = new String(nameBytes, Charset.forName("UTF-8")); 952 break; 953 default: 954 payload.position(payload.position() + len - 1); 955 break; 956 } 957 } 958 } catch (IllegalArgumentException e) { 959 Log.i(TAG, "BT OOB: invalid BT address"); 960 } catch (BufferUnderflowException e) { 961 Log.i(TAG, "BT OOB: payload shorter than expected"); 962 } 963 if (result.valid && result.name == null) result.name = ""; 964 return result; 965 } 966 967 static byte[] addressToReverseBytes(String address) { 968 String[] split = address.split(":"); 969 byte[] result = new byte[split.length]; 970 971 for (int i = 0; i < split.length; i++) { 972 // need to parse as int because parseByte() expects a signed byte 973 result[split.length - 1 - i] = (byte)Integer.parseInt(split[i], 16); 974 } 975 976 return result; 977 } 978 979 @Override 980 public void onServiceConnected(int profile, BluetoothProfile proxy) { 981 synchronized (HandoverManager.this) { 982 switch (profile) { 983 case BluetoothProfile.HEADSET: 984 mBluetoothHeadset = (BluetoothHeadset) proxy; 985 break; 986 case BluetoothProfile.A2DP: 987 mBluetoothA2dp = (BluetoothA2dp) proxy; 988 break; 989 } 990 } 991 } 992 993 @Override 994 public void onServiceDisconnected(int profile) { 995 synchronized (HandoverManager.this) { 996 switch (profile) { 997 case BluetoothProfile.HEADSET: 998 mBluetoothHeadset = null; 999 break; 1000 case BluetoothProfile.A2DP: 1001 mBluetoothA2dp = null; 1002 break; 1003 } 1004 } 1005 } 1006 1007 @Override 1008 public void onBluetoothHeadsetHandoverComplete(boolean connected) { 1009 synchronized (HandoverManager.this) { 1010 mBluetoothHeadsetHandover = null; 1011 mBluetoothHeadsetConnected = connected; 1012 } 1013 } 1014 1015 final BroadcastReceiver mReceiver = new BroadcastReceiver() { 1016 @Override 1017 public void onReceive(Context context, Intent intent) { 1018 String action = intent.getAction(); 1019 1020 if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { 1021 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); 1022 if (state == BluetoothAdapter.STATE_OFF) { 1023 mHandoverPowerManager.stopMonitoring(); 1024 } 1025 1026 return; 1027 } else if (action.equals(ACTION_CANCEL_HANDOVER_TRANSFER)) { 1028 String sourceAddress = intent.getStringExtra(EXTRA_SOURCE_ADDRESS); 1029 HandoverTransfer transfer = getOrCreateHandoverTransfer(sourceAddress, true, 1030 false); 1031 if (transfer != null) { 1032 transfer.cancel(); 1033 } 1034 } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS) || 1035 action.equals(ACTION_BT_OPP_TRANSFER_DONE)) { 1036 // Clean up old transfers no longer in progress 1037 cleanupTransfers(); 1038 1039 int direction = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_DIRECTION, -1); 1040 int id = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_ID, -1); 1041 String sourceAddress = intent.getStringExtra(EXTRA_BT_OPP_ADDRESS); 1042 1043 if (direction == -1 || id == -1 || sourceAddress == null) return; 1044 boolean incoming = (direction == DIRECTION_BLUETOOTH_INCOMING); 1045 1046 HandoverTransfer transfer = getOrCreateHandoverTransfer(sourceAddress, incoming, 1047 false); 1048 if (transfer == null) { 1049 // There is no transfer running for this source address; most likely 1050 // the transfer was cancelled. We need to tell BT OPP to stop transferring 1051 // in case this was an incoming transfer 1052 Intent cancelIntent = new Intent("android.btopp.intent.action.STOP_HANDOVER_TRANSFER"); 1053 cancelIntent.putExtra(EXTRA_BT_OPP_TRANSFER_ID, id); 1054 mContext.sendBroadcast(cancelIntent); 1055 return; 1056 } 1057 1058 if (action.equals(ACTION_BT_OPP_TRANSFER_DONE)) { 1059 int handoverStatus = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_STATUS, 1060 HANDOVER_TRANSFER_STATUS_FAILURE); 1061 1062 if (handoverStatus == HANDOVER_TRANSFER_STATUS_SUCCESS) { 1063 String uriString = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_URI); 1064 String mimeType = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_MIMETYPE); 1065 Uri uri = Uri.parse(uriString); 1066 if (uri.getScheme() == null) { 1067 uri = Uri.fromFile(new File(uri.getPath())); 1068 } 1069 transfer.finishTransfer(true, uri, mimeType); 1070 } else { 1071 transfer.finishTransfer(false, null, null); 1072 } 1073 } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS)) { 1074 float progress = intent.getFloatExtra(EXTRA_BT_OPP_TRANSFER_PROGRESS, 0.0f); 1075 transfer.updateFileProgress(progress); 1076 } 1077 } 1078 } 1079 }; 1080 1081 } 1082