1 /* 2 * Copyright (C) 2008 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.settings.bluetooth; 18 19 import android.app.AlertDialog; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothClass; 22 import android.bluetooth.BluetoothDevice; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.content.Intent; 26 import android.content.res.Resources; 27 import android.os.ParcelUuid; 28 import android.os.SystemClock; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import android.view.ContextMenu; 32 import android.view.Menu; 33 import android.view.MenuItem; 34 35 import com.android.settings.R; 36 import com.android.settings.bluetooth.LocalBluetoothProfileManager.Profile; 37 38 import java.text.DateFormat; 39 import java.util.ArrayList; 40 import java.util.Date; 41 import java.util.Iterator; 42 import java.util.LinkedList; 43 import java.util.List; 44 import java.util.Set; 45 46 /** 47 * CachedBluetoothDevice represents a remote Bluetooth device. It contains 48 * attributes of the device (such as the address, name, RSSI, etc.) and 49 * functionality that can be performed on the device (connect, pair, disconnect, 50 * etc.). 51 */ 52 public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { 53 private static final String TAG = "CachedBluetoothDevice"; 54 private static final boolean D = LocalBluetoothManager.D; 55 private static final boolean V = LocalBluetoothManager.V; 56 private static final boolean DEBUG = false; 57 58 private static final int CONTEXT_ITEM_CONNECT = Menu.FIRST + 1; 59 private static final int CONTEXT_ITEM_DISCONNECT = Menu.FIRST + 2; 60 private static final int CONTEXT_ITEM_UNPAIR = Menu.FIRST + 3; 61 private static final int CONTEXT_ITEM_CONNECT_ADVANCED = Menu.FIRST + 4; 62 63 private final BluetoothDevice mDevice; 64 private String mName; 65 private short mRssi; 66 private BluetoothClass mBtClass; 67 68 private List<Profile> mProfiles = new ArrayList<Profile>(); 69 70 private boolean mVisible; 71 72 private final LocalBluetoothManager mLocalManager; 73 74 private List<Callback> mCallbacks = new ArrayList<Callback>(); 75 76 /** 77 * When we connect to multiple profiles, we only want to display a single 78 * error even if they all fail. This tracks that state. 79 */ 80 private boolean mIsConnectingErrorPossible; 81 82 /** 83 * Last time a bt profile auto-connect was attempted. 84 * If an ACTION_UUID intent comes in within 85 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect 86 * again with the new UUIDs 87 */ 88 private long mConnectAttempted; 89 90 // See mConnectAttempted 91 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000; 92 93 // Max time to hold the work queue if we don't get or missed a response 94 // from the bt framework. 95 private static final long MAX_WAIT_TIME_FOR_FRAMEWORK = 25 * 1000; 96 97 private enum BluetoothCommand { 98 CONNECT, DISCONNECT, REMOVE_BOND, 99 } 100 101 static class BluetoothJob { 102 final BluetoothCommand command; // CONNECT, DISCONNECT 103 final CachedBluetoothDevice cachedDevice; 104 final Profile profile; // HEADSET, A2DP, etc 105 // 0 means this command was not been sent to the bt framework. 106 long timeSent; 107 108 public BluetoothJob(BluetoothCommand command, 109 CachedBluetoothDevice cachedDevice, Profile profile) { 110 this.command = command; 111 this.cachedDevice = cachedDevice; 112 this.profile = profile; 113 this.timeSent = 0; 114 } 115 116 @Override 117 public String toString() { 118 StringBuilder sb = new StringBuilder(); 119 sb.append(command.name()); 120 sb.append(" Address:").append(cachedDevice.mDevice); 121 if (profile != null) { 122 sb.append(" Profile:").append(profile.name()); 123 } 124 sb.append(" TimeSent:"); 125 if (timeSent == 0) { 126 sb.append("not yet"); 127 } else { 128 sb.append(DateFormat.getTimeInstance().format(new Date(timeSent))); 129 } 130 return sb.toString(); 131 } 132 } 133 134 /** 135 * We want to serialize connect and disconnect calls. http://b/170538 136 * This are some headsets that may have L2CAP resource limitation. We want 137 * to limit the bt bandwidth usage. 138 * 139 * A queue to keep track of asynchronous calls to the bt framework. The 140 * first item, if exist, should be in progress i.e. went to the bt framework 141 * already, waiting for a notification to come back. The second item and 142 * beyond have not been sent to the bt framework yet. 143 */ 144 private static LinkedList<BluetoothJob> workQueue = new LinkedList<BluetoothJob>(); 145 146 private void queueCommand(BluetoothJob job) { 147 synchronized (workQueue) { 148 if (D) { 149 Log.d(TAG, workQueue.toString()); 150 } 151 boolean processNow = pruneQueue(job); 152 153 // Add job to queue 154 if (D) { 155 Log.d(TAG, "Adding: " + job.toString()); 156 } 157 workQueue.add(job); 158 159 // if there's nothing pending from before, send the command to bt 160 // framework immediately. 161 if (workQueue.size() == 1 || processNow) { 162 // If the failed to process, just drop it from the queue. 163 // There will be no callback to remove this from the queue. 164 processCommands(); 165 } 166 } 167 } 168 169 private boolean pruneQueue(BluetoothJob job) { 170 boolean removedStaleItems = false; 171 long now = System.currentTimeMillis(); 172 Iterator<BluetoothJob> it = workQueue.iterator(); 173 while (it.hasNext()) { 174 BluetoothJob existingJob = it.next(); 175 176 // Remove any pending CONNECTS when we receive a DISCONNECT 177 if (job != null && job.command == BluetoothCommand.DISCONNECT) { 178 if (existingJob.timeSent == 0 179 && existingJob.command == BluetoothCommand.CONNECT 180 && existingJob.cachedDevice.mDevice.equals(job.cachedDevice.mDevice) 181 && existingJob.profile == job.profile) { 182 if (D) { 183 Log.d(TAG, "Removed because of a pending disconnect. " + existingJob); 184 } 185 it.remove(); 186 continue; 187 } 188 } 189 190 // Defensive Code: Remove any job that older than a preset time. 191 // We never got a call back. It is better to have overlapping 192 // calls than to get stuck. 193 if (existingJob.timeSent != 0 194 && (now - existingJob.timeSent) >= MAX_WAIT_TIME_FOR_FRAMEWORK) { 195 Log.w(TAG, "Timeout. Removing Job:" + existingJob.toString()); 196 it.remove(); 197 removedStaleItems = true; 198 continue; 199 } 200 } 201 return removedStaleItems; 202 } 203 204 private boolean processCommand(BluetoothJob job) { 205 boolean successful = false; 206 if (job.timeSent == 0) { 207 job.timeSent = System.currentTimeMillis(); 208 switch (job.command) { 209 case CONNECT: 210 successful = connectInt(job.cachedDevice, job.profile); 211 break; 212 case DISCONNECT: 213 successful = disconnectInt(job.cachedDevice, job.profile); 214 break; 215 case REMOVE_BOND: 216 BluetoothDevice dev = job.cachedDevice.getDevice(); 217 if (dev != null) { 218 successful = dev.removeBond(); 219 } 220 break; 221 } 222 223 if (successful) { 224 if (D) { 225 Log.d(TAG, "Command sent successfully:" + job.toString()); 226 } 227 } else if (V) { 228 Log.v(TAG, "Framework rejected command immediately:" + job.toString()); 229 } 230 } else if (D) { 231 Log.d(TAG, "Job already has a sent time. Skip. " + job.toString()); 232 } 233 234 return successful; 235 } 236 237 public void onProfileStateChanged(Profile profile, int newProfileState) { 238 synchronized (workQueue) { 239 if (D) { 240 Log.d(TAG, "onProfileStateChanged:" + workQueue.toString()); 241 } 242 243 int newState = LocalBluetoothProfileManager.getProfileManager(mLocalManager, 244 profile).convertState(newProfileState); 245 246 if (newState == SettingsBtStatus.CONNECTION_STATUS_CONNECTED) { 247 if (!mProfiles.contains(profile)) { 248 mProfiles.add(profile); 249 } 250 } 251 252 /* Ignore the transient states e.g. connecting, disconnecting */ 253 if (newState == SettingsBtStatus.CONNECTION_STATUS_CONNECTED || 254 newState == SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED) { 255 BluetoothJob job = workQueue.peek(); 256 if (job == null) { 257 return; 258 } else if (!job.cachedDevice.mDevice.equals(mDevice)) { 259 // This can happen in 2 cases: 1) BT device initiated pairing and 260 // 2) disconnects of one headset that's triggered by connects of 261 // another. 262 if (D) { 263 Log.d(TAG, "mDevice:" + mDevice + " != head:" + job.toString()); 264 } 265 266 // Check to see if we need to remove the stale items from the queue 267 if (!pruneQueue(null)) { 268 // nothing in the queue was modify. Just ignore the notification and return. 269 return; 270 } 271 } else { 272 // Remove the first item and process the next one 273 workQueue.poll(); 274 } 275 276 processCommands(); 277 } 278 } 279 } 280 281 /* 282 * This method is called in 2 places: 283 * 1) queryCommand() - when someone or something want to connect or 284 * disconnect 285 * 2) onProfileStateChanged() - when the framework sends an intent 286 * notification when it finishes processing a command 287 */ 288 private void processCommands() { 289 if (D) { 290 Log.d(TAG, "processCommands:" + workQueue.toString()); 291 } 292 Iterator<BluetoothJob> it = workQueue.iterator(); 293 while (it.hasNext()) { 294 BluetoothJob job = it.next(); 295 if (processCommand(job)) { 296 // Sent to bt framework. Done for now. Will remove this job 297 // from queue when we get an event 298 return; 299 } else { 300 /* 301 * If the command failed immediately, there will be no event 302 * callbacks. So delete the job immediately and move on to the 303 * next one 304 */ 305 it.remove(); 306 } 307 } 308 } 309 310 CachedBluetoothDevice(Context context, BluetoothDevice device) { 311 mLocalManager = LocalBluetoothManager.getInstance(context); 312 if (mLocalManager == null) { 313 throw new IllegalStateException( 314 "Cannot use CachedBluetoothDevice without Bluetooth hardware"); 315 } 316 317 mDevice = device; 318 319 fillData(); 320 } 321 322 public void onClicked() { 323 int bondState = getBondState(); 324 325 if (isConnected()) { 326 askDisconnect(); 327 } else if (bondState == BluetoothDevice.BOND_BONDED) { 328 connect(); 329 } else if (bondState == BluetoothDevice.BOND_NONE) { 330 pair(); 331 } 332 } 333 334 public void disconnect() { 335 for (Profile profile : mProfiles) { 336 disconnect(profile); 337 } 338 } 339 340 public void disconnect(Profile profile) { 341 queueCommand(new BluetoothJob(BluetoothCommand.DISCONNECT, this, profile)); 342 } 343 344 private boolean disconnectInt(CachedBluetoothDevice cachedDevice, Profile profile) { 345 LocalBluetoothProfileManager profileManager = 346 LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile); 347 int status = profileManager.getConnectionStatus(cachedDevice.mDevice); 348 if (SettingsBtStatus.isConnectionStatusConnected(status)) { 349 if (profileManager.disconnect(cachedDevice.mDevice)) { 350 return true; 351 } 352 } 353 return false; 354 } 355 356 public void askDisconnect() { 357 Context context = mLocalManager.getForegroundActivity(); 358 if (context == null) { 359 // Cannot ask, since we need an activity context 360 disconnect(); 361 return; 362 } 363 364 Resources res = context.getResources(); 365 366 String name = getName(); 367 if (TextUtils.isEmpty(name)) { 368 name = res.getString(R.string.bluetooth_device); 369 } 370 String message = res.getString(R.string.bluetooth_disconnect_blank, name); 371 372 DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() { 373 public void onClick(DialogInterface dialog, int which) { 374 disconnect(); 375 } 376 }; 377 378 new AlertDialog.Builder(context) 379 .setTitle(getName()) 380 .setMessage(message) 381 .setPositiveButton(android.R.string.ok, disconnectListener) 382 .setNegativeButton(android.R.string.cancel, null) 383 .show(); 384 } 385 386 public void connect() { 387 if (!ensurePaired()) return; 388 389 mConnectAttempted = SystemClock.elapsedRealtime(); 390 391 connectWithoutResettingTimer(); 392 } 393 394 /*package*/ void onBondingDockConnect() { 395 // Don't connect just set the timer. 396 // TODO(): Fix the actual problem 397 mConnectAttempted = SystemClock.elapsedRealtime(); 398 } 399 400 private void connectWithoutResettingTimer() { 401 // Try to initialize the profiles if there were not. 402 if (mProfiles.size() == 0) { 403 if (!updateProfiles()) { 404 // If UUIDs are not available yet, connect will be happen 405 // upon arrival of the ACTION_UUID intent. 406 if (DEBUG) Log.d(TAG, "No profiles. Maybe we will connect later"); 407 return; 408 } 409 } 410 411 // Reset the only-show-one-error-dialog tracking variable 412 mIsConnectingErrorPossible = true; 413 414 int preferredProfiles = 0; 415 for (Profile profile : mProfiles) { 416 if (isConnectableProfile(profile)) { 417 LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager 418 .getProfileManager(mLocalManager, profile); 419 if (profileManager.isPreferred(mDevice)) { 420 ++preferredProfiles; 421 disconnectConnected(profile); 422 queueCommand(new BluetoothJob(BluetoothCommand.CONNECT, this, profile)); 423 } 424 } 425 } 426 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles); 427 428 if (preferredProfiles == 0) { 429 connectAllProfiles(); 430 } 431 } 432 433 private void connectAllProfiles() { 434 if (!ensurePaired()) return; 435 436 // Reset the only-show-one-error-dialog tracking variable 437 mIsConnectingErrorPossible = true; 438 439 for (Profile profile : mProfiles) { 440 if (isConnectableProfile(profile)) { 441 LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager 442 .getProfileManager(mLocalManager, profile); 443 profileManager.setPreferred(mDevice, false); 444 disconnectConnected(profile); 445 queueCommand(new BluetoothJob(BluetoothCommand.CONNECT, this, profile)); 446 } 447 } 448 } 449 450 public void connect(Profile profile) { 451 mConnectAttempted = SystemClock.elapsedRealtime(); 452 // Reset the only-show-one-error-dialog tracking variable 453 mIsConnectingErrorPossible = true; 454 disconnectConnected(profile); 455 queueCommand(new BluetoothJob(BluetoothCommand.CONNECT, this, profile)); 456 } 457 458 private void disconnectConnected(Profile profile) { 459 LocalBluetoothProfileManager profileManager = 460 LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile); 461 CachedBluetoothDeviceManager cachedDeviceManager = mLocalManager.getCachedDeviceManager(); 462 Set<BluetoothDevice> devices = profileManager.getConnectedDevices(); 463 if (devices == null) return; 464 for (BluetoothDevice device : devices) { 465 CachedBluetoothDevice cachedDevice = cachedDeviceManager.findDevice(device); 466 if (cachedDevice != null) { 467 queueCommand(new BluetoothJob(BluetoothCommand.DISCONNECT, cachedDevice, profile)); 468 } 469 } 470 } 471 472 private boolean connectInt(CachedBluetoothDevice cachedDevice, Profile profile) { 473 if (!cachedDevice.ensurePaired()) return false; 474 475 LocalBluetoothProfileManager profileManager = 476 LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile); 477 int status = profileManager.getConnectionStatus(cachedDevice.mDevice); 478 if (!SettingsBtStatus.isConnectionStatusConnected(status)) { 479 if (profileManager.connect(cachedDevice.mDevice)) { 480 return true; 481 } 482 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + cachedDevice.mName); 483 } else { 484 Log.i(TAG, "Already connected"); 485 } 486 return false; 487 } 488 489 public void showConnectingError() { 490 if (!mIsConnectingErrorPossible) return; 491 mIsConnectingErrorPossible = false; 492 493 mLocalManager.showError(mDevice, R.string.bluetooth_error_title, 494 R.string.bluetooth_connecting_error_message); 495 } 496 497 private boolean ensurePaired() { 498 if (getBondState() == BluetoothDevice.BOND_NONE) { 499 pair(); 500 return false; 501 } else { 502 return true; 503 } 504 } 505 506 public void pair() { 507 BluetoothAdapter adapter = mLocalManager.getBluetoothAdapter(); 508 509 // Pairing is unreliable while scanning, so cancel discovery 510 if (adapter.isDiscovering()) { 511 adapter.cancelDiscovery(); 512 } 513 514 if (!mDevice.createBond()) { 515 mLocalManager.showError(mDevice, R.string.bluetooth_error_title, 516 R.string.bluetooth_pairing_error_message); 517 } 518 } 519 520 public void unpair() { 521 disconnect(); 522 523 int state = getBondState(); 524 525 if (state == BluetoothDevice.BOND_BONDING) { 526 mDevice.cancelBondProcess(); 527 } 528 529 if (state != BluetoothDevice.BOND_NONE) { 530 queueCommand(new BluetoothJob(BluetoothCommand.REMOVE_BOND, this, null)); 531 } 532 } 533 534 private void fillData() { 535 fetchName(); 536 fetchBtClass(); 537 updateProfiles(); 538 539 mVisible = false; 540 541 dispatchAttributesChanged(); 542 } 543 544 public BluetoothDevice getDevice() { 545 return mDevice; 546 } 547 548 public String getName() { 549 return mName; 550 } 551 552 public void setName(String name) { 553 if (!mName.equals(name)) { 554 if (TextUtils.isEmpty(name)) { 555 mName = mDevice.getAddress(); 556 } else { 557 mName = name; 558 } 559 dispatchAttributesChanged(); 560 } 561 } 562 563 public void refreshName() { 564 fetchName(); 565 dispatchAttributesChanged(); 566 } 567 568 private void fetchName() { 569 mName = mDevice.getName(); 570 571 if (TextUtils.isEmpty(mName)) { 572 mName = mDevice.getAddress(); 573 if (DEBUG) Log.d(TAG, "Default to address. Device has no name (yet) " + mName); 574 } 575 } 576 577 public void refresh() { 578 dispatchAttributesChanged(); 579 } 580 581 public boolean isVisible() { 582 return mVisible; 583 } 584 585 void setVisible(boolean visible) { 586 if (mVisible != visible) { 587 mVisible = visible; 588 dispatchAttributesChanged(); 589 } 590 } 591 592 public int getBondState() { 593 return mDevice.getBondState(); 594 } 595 596 void setRssi(short rssi) { 597 if (mRssi != rssi) { 598 mRssi = rssi; 599 dispatchAttributesChanged(); 600 } 601 } 602 603 /** 604 * Checks whether we are connected to this device (any profile counts). 605 * 606 * @return Whether it is connected. 607 */ 608 public boolean isConnected() { 609 for (Profile profile : mProfiles) { 610 int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile) 611 .getConnectionStatus(mDevice); 612 if (SettingsBtStatus.isConnectionStatusConnected(status)) { 613 return true; 614 } 615 } 616 617 return false; 618 } 619 620 public boolean isBusy() { 621 for (Profile profile : mProfiles) { 622 int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile) 623 .getConnectionStatus(mDevice); 624 if (SettingsBtStatus.isConnectionStatusBusy(status)) { 625 return true; 626 } 627 } 628 629 if (getBondState() == BluetoothDevice.BOND_BONDING) { 630 return true; 631 } 632 633 return false; 634 } 635 636 public int getBtClassDrawable() { 637 if (mBtClass != null) { 638 switch (mBtClass.getMajorDeviceClass()) { 639 case BluetoothClass.Device.Major.COMPUTER: 640 return R.drawable.ic_bt_laptop; 641 642 case BluetoothClass.Device.Major.PHONE: 643 return R.drawable.ic_bt_cellphone; 644 } 645 } else { 646 Log.w(TAG, "mBtClass is null"); 647 } 648 649 if (mProfiles.size() > 0) { 650 if (mProfiles.contains(Profile.A2DP)) { 651 return R.drawable.ic_bt_headphones_a2dp; 652 } else if (mProfiles.contains(Profile.HEADSET)) { 653 return R.drawable.ic_bt_headset_hfp; 654 } 655 } else if (mBtClass != null) { 656 if (mBtClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) { 657 return R.drawable.ic_bt_headphones_a2dp; 658 659 } 660 if (mBtClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) { 661 return R.drawable.ic_bt_headset_hfp; 662 } 663 } 664 return 0; 665 } 666 667 /** 668 * Fetches a new value for the cached BT class. 669 */ 670 private void fetchBtClass() { 671 mBtClass = mDevice.getBluetoothClass(); 672 } 673 674 private boolean updateProfiles() { 675 ParcelUuid[] uuids = mDevice.getUuids(); 676 if (uuids == null) return false; 677 678 LocalBluetoothProfileManager.updateProfiles(uuids, mProfiles); 679 680 if (DEBUG) { 681 Log.e(TAG, "updating profiles for " + mDevice.getName()); 682 683 boolean printUuids = true; 684 BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); 685 686 if (bluetoothClass != null) { 687 if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET) != 688 mProfiles.contains(Profile.HEADSET)) { 689 Log.v(TAG, "headset classbits != uuid"); 690 printUuids = true; 691 } 692 693 if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_A2DP) != 694 mProfiles.contains(Profile.A2DP)) { 695 Log.v(TAG, "a2dp classbits != uuid"); 696 printUuids = true; 697 } 698 699 if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_OPP) != 700 mProfiles.contains(Profile.OPP)) { 701 Log.v(TAG, "opp classbits != uuid"); 702 printUuids = true; 703 } 704 } 705 706 if (printUuids) { 707 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString()); 708 Log.v(TAG, "UUID:"); 709 for (int i = 0; i < uuids.length; i++) { 710 Log.v(TAG, " " + uuids[i]); 711 } 712 } 713 } 714 return true; 715 } 716 717 /** 718 * Refreshes the UI for the BT class, including fetching the latest value 719 * for the class. 720 */ 721 public void refreshBtClass() { 722 fetchBtClass(); 723 dispatchAttributesChanged(); 724 } 725 726 /** 727 * Refreshes the UI when framework alerts us of a UUID change. 728 */ 729 public void onUuidChanged() { 730 updateProfiles(); 731 732 if (DEBUG) { 733 Log.e(TAG, "onUuidChanged: Time since last connect" 734 + (SystemClock.elapsedRealtime() - mConnectAttempted)); 735 } 736 737 /* 738 * If a connect was attempted earlier without any UUID, we will do the 739 * connect now. 740 */ 741 if (mProfiles.size() > 0 742 && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock 743 .elapsedRealtime()) { 744 connectWithoutResettingTimer(); 745 } 746 dispatchAttributesChanged(); 747 } 748 749 public void onBondingStateChanged(int bondState) { 750 if (bondState == BluetoothDevice.BOND_NONE) { 751 mProfiles.clear(); 752 753 BluetoothJob job = workQueue.peek(); 754 if (job != null) { 755 // Remove the first item and process the next one 756 if (job.command == BluetoothCommand.REMOVE_BOND 757 && job.cachedDevice.mDevice.equals(mDevice)) { 758 workQueue.poll(); // dequeue 759 } else { 760 // Unexpected job 761 if (D) { 762 Log.d(TAG, "job.command = " + job.command); 763 Log.d(TAG, "mDevice:" + mDevice + " != head:" + job.toString()); 764 } 765 766 // Check to see if we need to remove the stale items from the queue 767 if (!pruneQueue(null)) { 768 // nothing in the queue was modify. Just ignore the notification and return. 769 refresh(); 770 return; 771 } 772 } 773 774 processCommands(); 775 } 776 } 777 778 refresh(); 779 } 780 781 public void setBtClass(BluetoothClass btClass) { 782 if (btClass != null && mBtClass != btClass) { 783 mBtClass = btClass; 784 dispatchAttributesChanged(); 785 } 786 } 787 788 public int getSummary() { 789 // TODO: clean up 790 int oneOffSummary = getOneOffSummary(); 791 if (oneOffSummary != 0) { 792 return oneOffSummary; 793 } 794 795 for (Profile profile : mProfiles) { 796 LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager 797 .getProfileManager(mLocalManager, profile); 798 int connectionStatus = profileManager.getConnectionStatus(mDevice); 799 800 if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus) || 801 connectionStatus == SettingsBtStatus.CONNECTION_STATUS_CONNECTING || 802 connectionStatus == SettingsBtStatus.CONNECTION_STATUS_DISCONNECTING) { 803 return SettingsBtStatus.getConnectionStatusSummary(connectionStatus); 804 } 805 } 806 807 return SettingsBtStatus.getPairingStatusSummary(getBondState()); 808 } 809 810 /** 811 * We have special summaries when particular profiles are connected. This 812 * checks for those states and returns an applicable summary. 813 * 814 * @return A one-off summary that is applicable for the current state, or 0. 815 */ 816 private int getOneOffSummary() { 817 boolean isA2dpConnected = false, isHeadsetConnected = false, isConnecting = false; 818 819 if (mProfiles.contains(Profile.A2DP)) { 820 LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager 821 .getProfileManager(mLocalManager, Profile.A2DP); 822 isConnecting = profileManager.getConnectionStatus(mDevice) == 823 SettingsBtStatus.CONNECTION_STATUS_CONNECTING; 824 isA2dpConnected = profileManager.isConnected(mDevice); 825 } 826 827 if (mProfiles.contains(Profile.HEADSET)) { 828 LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager 829 .getProfileManager(mLocalManager, Profile.HEADSET); 830 isConnecting |= profileManager.getConnectionStatus(mDevice) == 831 SettingsBtStatus.CONNECTION_STATUS_CONNECTING; 832 isHeadsetConnected = profileManager.isConnected(mDevice); 833 } 834 835 if (isConnecting) { 836 // If any of these important profiles is connecting, prefer that 837 return SettingsBtStatus.getConnectionStatusSummary( 838 SettingsBtStatus.CONNECTION_STATUS_CONNECTING); 839 } else if (isA2dpConnected && isHeadsetConnected) { 840 return R.string.bluetooth_summary_connected_to_a2dp_headset; 841 } else if (isA2dpConnected) { 842 return R.string.bluetooth_summary_connected_to_a2dp; 843 } else if (isHeadsetConnected) { 844 return R.string.bluetooth_summary_connected_to_headset; 845 } else { 846 return 0; 847 } 848 } 849 850 public List<Profile> getConnectableProfiles() { 851 ArrayList<Profile> connectableProfiles = new ArrayList<Profile>(); 852 for (Profile profile : mProfiles) { 853 if (isConnectableProfile(profile)) { 854 connectableProfiles.add(profile); 855 } 856 } 857 return connectableProfiles; 858 } 859 860 private boolean isConnectableProfile(Profile profile) { 861 return profile.equals(Profile.HEADSET) || profile.equals(Profile.A2DP); 862 } 863 864 public void onCreateContextMenu(ContextMenu menu) { 865 // No context menu if it is busy (none of these items are applicable if busy) 866 if (mLocalManager.getBluetoothState() != BluetoothAdapter.STATE_ON || isBusy()) { 867 return; 868 } 869 870 int bondState = getBondState(); 871 boolean isConnected = isConnected(); 872 boolean hasConnectableProfiles = false; 873 874 for (Profile profile : mProfiles) { 875 if (isConnectableProfile(profile)) { 876 hasConnectableProfiles = true; 877 break; 878 } 879 } 880 881 menu.setHeaderTitle(getName()); 882 883 if (bondState == BluetoothDevice.BOND_NONE) { // Not paired and not connected 884 menu.add(0, CONTEXT_ITEM_CONNECT, 0, R.string.bluetooth_device_context_pair_connect); 885 } else { // Paired 886 if (isConnected) { // Paired and connected 887 menu.add(0, CONTEXT_ITEM_DISCONNECT, 0, 888 R.string.bluetooth_device_context_disconnect); 889 menu.add(0, CONTEXT_ITEM_UNPAIR, 0, 890 R.string.bluetooth_device_context_disconnect_unpair); 891 } else { // Paired but not connected 892 if (hasConnectableProfiles) { 893 menu.add(0, CONTEXT_ITEM_CONNECT, 0, R.string.bluetooth_device_context_connect); 894 } 895 menu.add(0, CONTEXT_ITEM_UNPAIR, 0, R.string.bluetooth_device_context_unpair); 896 } 897 898 // Show the connection options item 899 if (hasConnectableProfiles) { 900 menu.add(0, CONTEXT_ITEM_CONNECT_ADVANCED, 0, 901 R.string.bluetooth_device_context_connect_advanced); 902 } 903 } 904 } 905 906 /** 907 * Called when a context menu item is clicked. 908 * 909 * @param item The item that was clicked. 910 */ 911 public void onContextItemSelected(MenuItem item) { 912 switch (item.getItemId()) { 913 case CONTEXT_ITEM_DISCONNECT: 914 disconnect(); 915 break; 916 917 case CONTEXT_ITEM_CONNECT: 918 connect(); 919 break; 920 921 case CONTEXT_ITEM_UNPAIR: 922 unpair(); 923 break; 924 925 case CONTEXT_ITEM_CONNECT_ADVANCED: 926 Intent intent = new Intent(); 927 // Need an activity context to open this in our task 928 Context context = mLocalManager.getForegroundActivity(); 929 if (context == null) { 930 // Fallback on application context, and open in a new task 931 context = mLocalManager.getContext(); 932 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 933 } 934 intent.setClass(context, ConnectSpecificProfilesActivity.class); 935 intent.putExtra(ConnectSpecificProfilesActivity.EXTRA_DEVICE, mDevice); 936 context.startActivity(intent); 937 break; 938 } 939 } 940 941 public void registerCallback(Callback callback) { 942 synchronized (mCallbacks) { 943 mCallbacks.add(callback); 944 } 945 } 946 947 public void unregisterCallback(Callback callback) { 948 synchronized (mCallbacks) { 949 mCallbacks.remove(callback); 950 } 951 } 952 953 private void dispatchAttributesChanged() { 954 synchronized (mCallbacks) { 955 for (Callback callback : mCallbacks) { 956 callback.onDeviceAttributesChanged(this); 957 } 958 } 959 } 960 961 @Override 962 public String toString() { 963 return mDevice.toString(); 964 } 965 966 @Override 967 public boolean equals(Object o) { 968 if ((o == null) || !(o instanceof CachedBluetoothDevice)) { 969 throw new ClassCastException(); 970 } 971 972 return mDevice.equals(((CachedBluetoothDevice) o).mDevice); 973 } 974 975 @Override 976 public int hashCode() { 977 return mDevice.getAddress().hashCode(); 978 } 979 980 public int compareTo(CachedBluetoothDevice another) { 981 int comparison; 982 983 // Connected above not connected 984 comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0); 985 if (comparison != 0) return comparison; 986 987 // Paired above not paired 988 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) - 989 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0); 990 if (comparison != 0) return comparison; 991 992 // Visible above not visible 993 comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0); 994 if (comparison != 0) return comparison; 995 996 // Stronger signal above weaker signal 997 comparison = another.mRssi - mRssi; 998 if (comparison != 0) return comparison; 999 1000 // Fallback on name 1001 return getName().compareTo(another.getName()); 1002 } 1003 1004 public interface Callback { 1005 void onDeviceAttributesChanged(CachedBluetoothDevice cachedDevice); 1006 } 1007 } 1008