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