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.bluetooth.BluetoothClass; 20 import android.bluetooth.BluetoothDevice; 21 import android.bluetooth.BluetoothProfile; 22 import android.content.Context; 23 import android.content.SharedPreferences; 24 import android.os.ParcelUuid; 25 import android.os.SystemClock; 26 import android.text.TextUtils; 27 import android.util.Log; 28 import android.bluetooth.BluetoothAdapter; 29 30 import java.util.ArrayList; 31 import java.util.Collection; 32 import java.util.Collections; 33 import java.util.HashMap; 34 import java.util.List; 35 36 /** 37 * CachedBluetoothDevice represents a remote Bluetooth device. It contains 38 * attributes of the device (such as the address, name, RSSI, etc.) and 39 * functionality that can be performed on the device (connect, pair, disconnect, 40 * etc.). 41 */ 42 final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { 43 private static final String TAG = "CachedBluetoothDevice"; 44 private static final boolean DEBUG = Utils.V; 45 46 private final Context mContext; 47 private final LocalBluetoothAdapter mLocalAdapter; 48 private final LocalBluetoothProfileManager mProfileManager; 49 private final BluetoothDevice mDevice; 50 private String mName; 51 private short mRssi; 52 private BluetoothClass mBtClass; 53 private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState; 54 55 private final List<LocalBluetoothProfile> mProfiles = 56 new ArrayList<LocalBluetoothProfile>(); 57 58 // List of profiles that were previously in mProfiles, but have been removed 59 private final List<LocalBluetoothProfile> mRemovedProfiles = 60 new ArrayList<LocalBluetoothProfile>(); 61 62 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP 63 private boolean mLocalNapRoleConnected; 64 65 private boolean mVisible; 66 67 private int mPhonebookPermissionChoice; 68 69 private final Collection<Callback> mCallbacks = new ArrayList<Callback>(); 70 71 // Following constants indicate the user's choices of Phone book access settings 72 // User hasn't made any choice or settings app has wiped out the memory 73 final static int PHONEBOOK_ACCESS_UNKNOWN = 0; 74 // User has accepted the connection and let Settings app remember the decision 75 final static int PHONEBOOK_ACCESS_ALLOWED = 1; 76 // User has rejected the connection and let Settings app remember the decision 77 final static int PHONEBOOK_ACCESS_REJECTED = 2; 78 79 private final static String PHONEBOOK_PREFS_NAME = "bluetooth_phonebook_permission"; 80 81 /** 82 * When we connect to multiple profiles, we only want to display a single 83 * error even if they all fail. This tracks that state. 84 */ 85 private boolean mIsConnectingErrorPossible; 86 87 /** 88 * Last time a bt profile auto-connect was attempted. 89 * If an ACTION_UUID intent comes in within 90 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect 91 * again with the new UUIDs 92 */ 93 private long mConnectAttempted; 94 95 // See mConnectAttempted 96 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000; 97 98 /** Auto-connect after pairing only if locally initiated. */ 99 private boolean mConnectAfterPairing; 100 101 /** 102 * Describes the current device and profile for logging. 103 * 104 * @param profile Profile to describe 105 * @return Description of the device and profile 106 */ 107 private String describe(LocalBluetoothProfile profile) { 108 StringBuilder sb = new StringBuilder(); 109 sb.append("Address:").append(mDevice); 110 if (profile != null) { 111 sb.append(" Profile:").append(profile); 112 } 113 114 return sb.toString(); 115 } 116 117 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) { 118 if (Utils.D) { 119 Log.d(TAG, "onProfileStateChanged: profile " + profile + 120 " newProfileState " + newProfileState); 121 } 122 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF) 123 { 124 if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored..."); 125 return; 126 } 127 mProfileConnectionState.put(profile, newProfileState); 128 if (newProfileState == BluetoothProfile.STATE_CONNECTED) { 129 if (!mProfiles.contains(profile)) { 130 mRemovedProfiles.remove(profile); 131 mProfiles.add(profile); 132 if (profile instanceof PanProfile && 133 ((PanProfile) profile).isLocalRoleNap(mDevice)) { 134 // Device doesn't support NAP, so remove PanProfile on disconnect 135 mLocalNapRoleConnected = true; 136 } 137 } 138 } else if (mLocalNapRoleConnected && profile instanceof PanProfile && 139 ((PanProfile) profile).isLocalRoleNap(mDevice) && 140 newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 141 Log.d(TAG, "Removing PanProfile from device after NAP disconnect"); 142 mProfiles.remove(profile); 143 mRemovedProfiles.add(profile); 144 mLocalNapRoleConnected = false; 145 } 146 } 147 148 CachedBluetoothDevice(Context context, 149 LocalBluetoothAdapter adapter, 150 LocalBluetoothProfileManager profileManager, 151 BluetoothDevice device) { 152 mContext = context; 153 mLocalAdapter = adapter; 154 mProfileManager = profileManager; 155 mDevice = device; 156 mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>(); 157 fillData(); 158 } 159 160 void disconnect() { 161 for (LocalBluetoothProfile profile : mProfiles) { 162 disconnect(profile); 163 } 164 // Disconnect PBAP server in case its connected 165 // This is to ensure all the profiles are disconnected as some CK/Hs do not 166 // disconnect PBAP connection when HF connection is brought down 167 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile(); 168 if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED) 169 { 170 PbapProfile.disconnect(mDevice); 171 } 172 } 173 174 void disconnect(LocalBluetoothProfile profile) { 175 if (profile.disconnect(mDevice)) { 176 if (Utils.D) { 177 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile)); 178 } 179 } 180 } 181 182 void connect(boolean connectAllProfiles) { 183 if (!ensurePaired()) { 184 return; 185 } 186 187 mConnectAttempted = SystemClock.elapsedRealtime(); 188 connectWithoutResettingTimer(connectAllProfiles); 189 } 190 191 void onBondingDockConnect() { 192 // Attempt to connect if UUIDs are available. Otherwise, 193 // we will connect when the ACTION_UUID intent arrives. 194 connect(false); 195 } 196 197 private void connectWithoutResettingTimer(boolean connectAllProfiles) { 198 // Try to initialize the profiles if they were not. 199 if (mProfiles.isEmpty()) { 200 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race 201 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated 202 // from bluetooth stack but ACTION.uuid is not sent yet. 203 // Eventually ACTION.uuid will be received which shall trigger the connection of the 204 // various profiles 205 // If UUIDs are not available yet, connect will be happen 206 // upon arrival of the ACTION_UUID intent. 207 Log.d(TAG, "No profiles. Maybe we will connect later"); 208 return; 209 } 210 211 // Reset the only-show-one-error-dialog tracking variable 212 mIsConnectingErrorPossible = true; 213 214 int preferredProfiles = 0; 215 for (LocalBluetoothProfile profile : mProfiles) { 216 if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) { 217 if (profile.isPreferred(mDevice)) { 218 ++preferredProfiles; 219 connectInt(profile); 220 } 221 } 222 } 223 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles); 224 225 if (preferredProfiles == 0) { 226 connectAutoConnectableProfiles(); 227 } 228 } 229 230 private void connectAutoConnectableProfiles() { 231 if (!ensurePaired()) { 232 return; 233 } 234 // Reset the only-show-one-error-dialog tracking variable 235 mIsConnectingErrorPossible = true; 236 237 for (LocalBluetoothProfile profile : mProfiles) { 238 if (profile.isAutoConnectable()) { 239 profile.setPreferred(mDevice, true); 240 connectInt(profile); 241 } 242 } 243 } 244 245 /** 246 * Connect this device to the specified profile. 247 * 248 * @param profile the profile to use with the remote device 249 */ 250 void connectProfile(LocalBluetoothProfile profile) { 251 mConnectAttempted = SystemClock.elapsedRealtime(); 252 // Reset the only-show-one-error-dialog tracking variable 253 mIsConnectingErrorPossible = true; 254 connectInt(profile); 255 // Refresh the UI based on profile.connect() call 256 refresh(); 257 } 258 259 synchronized void connectInt(LocalBluetoothProfile profile) { 260 if (!ensurePaired()) { 261 return; 262 } 263 if (profile.connect(mDevice)) { 264 if (Utils.D) { 265 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile)); 266 } 267 return; 268 } 269 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName); 270 } 271 272 private boolean ensurePaired() { 273 if (getBondState() == BluetoothDevice.BOND_NONE) { 274 startPairing(); 275 return false; 276 } else { 277 return true; 278 } 279 } 280 281 boolean startPairing() { 282 // Pairing is unreliable while scanning, so cancel discovery 283 if (mLocalAdapter.isDiscovering()) { 284 mLocalAdapter.cancelDiscovery(); 285 } 286 287 if (!mDevice.createBond()) { 288 return false; 289 } 290 291 mConnectAfterPairing = true; // auto-connect after pairing 292 return true; 293 } 294 295 /** 296 * Return true if user initiated pairing on this device. The message text is 297 * slightly different for local vs. remote initiated pairing dialogs. 298 */ 299 boolean isUserInitiatedPairing() { 300 return mConnectAfterPairing; 301 } 302 303 void unpair() { 304 int state = getBondState(); 305 306 if (state == BluetoothDevice.BOND_BONDING) { 307 mDevice.cancelBondProcess(); 308 } 309 310 if (state != BluetoothDevice.BOND_NONE) { 311 final BluetoothDevice dev = mDevice; 312 if (dev != null) { 313 final boolean successful = dev.removeBond(); 314 if (successful) { 315 if (Utils.D) { 316 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null)); 317 } 318 } else if (Utils.V) { 319 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " + 320 describe(null)); 321 } 322 } 323 } 324 } 325 326 int getProfileConnectionState(LocalBluetoothProfile profile) { 327 if (mProfileConnectionState == null || 328 mProfileConnectionState.get(profile) == null) { 329 // If cache is empty make the binder call to get the state 330 int state = profile.getConnectionStatus(mDevice); 331 mProfileConnectionState.put(profile, state); 332 } 333 return mProfileConnectionState.get(profile); 334 } 335 336 public void clearProfileConnectionState () 337 { 338 if (Utils.D) { 339 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName()); 340 } 341 for (LocalBluetoothProfile profile :getProfiles()) { 342 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED); 343 } 344 } 345 346 // TODO: do any of these need to run async on a background thread? 347 private void fillData() { 348 fetchName(); 349 fetchBtClass(); 350 updateProfiles(); 351 fetchPhonebookPermissionChoice(); 352 353 mVisible = false; 354 dispatchAttributesChanged(); 355 } 356 357 BluetoothDevice getDevice() { 358 return mDevice; 359 } 360 361 String getName() { 362 return mName; 363 } 364 365 void setName(String name) { 366 if (!mName.equals(name)) { 367 if (TextUtils.isEmpty(name)) { 368 // TODO: use friendly name for unknown device (bug 1181856) 369 mName = mDevice.getAddress(); 370 } else { 371 mName = name; 372 mDevice.setAlias(name); 373 } 374 dispatchAttributesChanged(); 375 } 376 } 377 378 void refreshName() { 379 fetchName(); 380 dispatchAttributesChanged(); 381 } 382 383 private void fetchName() { 384 mName = mDevice.getAliasName(); 385 386 if (TextUtils.isEmpty(mName)) { 387 mName = mDevice.getAddress(); 388 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName); 389 } 390 } 391 392 void refresh() { 393 dispatchAttributesChanged(); 394 } 395 396 boolean isVisible() { 397 return mVisible; 398 } 399 400 void setVisible(boolean visible) { 401 if (mVisible != visible) { 402 mVisible = visible; 403 dispatchAttributesChanged(); 404 } 405 } 406 407 int getBondState() { 408 return mDevice.getBondState(); 409 } 410 411 void setRssi(short rssi) { 412 if (mRssi != rssi) { 413 mRssi = rssi; 414 dispatchAttributesChanged(); 415 } 416 } 417 418 /** 419 * Checks whether we are connected to this device (any profile counts). 420 * 421 * @return Whether it is connected. 422 */ 423 boolean isConnected() { 424 for (LocalBluetoothProfile profile : mProfiles) { 425 int status = getProfileConnectionState(profile); 426 if (status == BluetoothProfile.STATE_CONNECTED) { 427 return true; 428 } 429 } 430 431 return false; 432 } 433 434 boolean isConnectedProfile(LocalBluetoothProfile profile) { 435 int status = getProfileConnectionState(profile); 436 return status == BluetoothProfile.STATE_CONNECTED; 437 438 } 439 440 boolean isBusy() { 441 for (LocalBluetoothProfile profile : mProfiles) { 442 int status = getProfileConnectionState(profile); 443 if (status == BluetoothProfile.STATE_CONNECTING 444 || status == BluetoothProfile.STATE_DISCONNECTING) { 445 return true; 446 } 447 } 448 return getBondState() == BluetoothDevice.BOND_BONDING; 449 } 450 451 /** 452 * Fetches a new value for the cached BT class. 453 */ 454 private void fetchBtClass() { 455 mBtClass = mDevice.getBluetoothClass(); 456 } 457 458 private boolean updateProfiles() { 459 ParcelUuid[] uuids = mDevice.getUuids(); 460 if (uuids == null) return false; 461 462 ParcelUuid[] localUuids = mLocalAdapter.getUuids(); 463 if (localUuids == null) return false; 464 465 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles, mLocalNapRoleConnected); 466 467 if (DEBUG) { 468 Log.e(TAG, "updating profiles for " + mDevice.getAliasName()); 469 BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); 470 471 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString()); 472 Log.v(TAG, "UUID:"); 473 for (ParcelUuid uuid : uuids) { 474 Log.v(TAG, " " + uuid); 475 } 476 } 477 return true; 478 } 479 480 /** 481 * Refreshes the UI for the BT class, including fetching the latest value 482 * for the class. 483 */ 484 void refreshBtClass() { 485 fetchBtClass(); 486 dispatchAttributesChanged(); 487 } 488 489 /** 490 * Refreshes the UI when framework alerts us of a UUID change. 491 */ 492 void onUuidChanged() { 493 updateProfiles(); 494 495 if (DEBUG) { 496 Log.e(TAG, "onUuidChanged: Time since last connect" 497 + (SystemClock.elapsedRealtime() - mConnectAttempted)); 498 } 499 500 /* 501 * If a connect was attempted earlier without any UUID, we will do the 502 * connect now. 503 */ 504 if (!mProfiles.isEmpty() 505 && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock 506 .elapsedRealtime()) { 507 connectWithoutResettingTimer(false); 508 } 509 dispatchAttributesChanged(); 510 } 511 512 void onBondingStateChanged(int bondState) { 513 if (bondState == BluetoothDevice.BOND_NONE) { 514 mProfiles.clear(); 515 mConnectAfterPairing = false; // cancel auto-connect 516 setPhonebookPermissionChoice(PHONEBOOK_ACCESS_UNKNOWN); 517 } 518 519 refresh(); 520 521 if (bondState == BluetoothDevice.BOND_BONDED) { 522 if (mDevice.isBluetoothDock()) { 523 onBondingDockConnect(); 524 } else if (mConnectAfterPairing) { 525 connect(false); 526 } 527 mConnectAfterPairing = false; 528 } 529 } 530 531 void setBtClass(BluetoothClass btClass) { 532 if (btClass != null && mBtClass != btClass) { 533 mBtClass = btClass; 534 dispatchAttributesChanged(); 535 } 536 } 537 538 BluetoothClass getBtClass() { 539 return mBtClass; 540 } 541 542 List<LocalBluetoothProfile> getProfiles() { 543 return Collections.unmodifiableList(mProfiles); 544 } 545 546 List<LocalBluetoothProfile> getConnectableProfiles() { 547 List<LocalBluetoothProfile> connectableProfiles = 548 new ArrayList<LocalBluetoothProfile>(); 549 for (LocalBluetoothProfile profile : mProfiles) { 550 if (profile.isConnectable()) { 551 connectableProfiles.add(profile); 552 } 553 } 554 return connectableProfiles; 555 } 556 557 List<LocalBluetoothProfile> getRemovedProfiles() { 558 return mRemovedProfiles; 559 } 560 561 void registerCallback(Callback callback) { 562 synchronized (mCallbacks) { 563 mCallbacks.add(callback); 564 } 565 } 566 567 void unregisterCallback(Callback callback) { 568 synchronized (mCallbacks) { 569 mCallbacks.remove(callback); 570 } 571 } 572 573 private void dispatchAttributesChanged() { 574 synchronized (mCallbacks) { 575 for (Callback callback : mCallbacks) { 576 callback.onDeviceAttributesChanged(); 577 } 578 } 579 } 580 581 @Override 582 public String toString() { 583 return mDevice.toString(); 584 } 585 586 @Override 587 public boolean equals(Object o) { 588 if ((o == null) || !(o instanceof CachedBluetoothDevice)) { 589 return false; 590 } 591 return mDevice.equals(((CachedBluetoothDevice) o).mDevice); 592 } 593 594 @Override 595 public int hashCode() { 596 return mDevice.getAddress().hashCode(); 597 } 598 599 // This comparison uses non-final fields so the sort order may change 600 // when device attributes change (such as bonding state). Settings 601 // will completely refresh the device list when this happens. 602 public int compareTo(CachedBluetoothDevice another) { 603 // Connected above not connected 604 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0); 605 if (comparison != 0) return comparison; 606 607 // Paired above not paired 608 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) - 609 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0); 610 if (comparison != 0) return comparison; 611 612 // Visible above not visible 613 comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0); 614 if (comparison != 0) return comparison; 615 616 // Stronger signal above weaker signal 617 comparison = another.mRssi - mRssi; 618 if (comparison != 0) return comparison; 619 620 // Fallback on name 621 return mName.compareTo(another.mName); 622 } 623 624 public interface Callback { 625 void onDeviceAttributesChanged(); 626 } 627 628 int getPhonebookPermissionChoice() { 629 return mPhonebookPermissionChoice; 630 } 631 632 void setPhonebookPermissionChoice(int permissionChoice) { 633 SharedPreferences.Editor editor = 634 mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME, Context.MODE_PRIVATE).edit(); 635 if (permissionChoice == PHONEBOOK_ACCESS_UNKNOWN) { 636 editor.remove(mDevice.getAddress()); 637 } else { 638 editor.putInt(mDevice.getAddress(), permissionChoice); 639 } 640 editor.commit(); 641 mPhonebookPermissionChoice = permissionChoice; 642 } 643 644 private void fetchPhonebookPermissionChoice() { 645 SharedPreferences preference = mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME, 646 Context.MODE_PRIVATE); 647 mPhonebookPermissionChoice = preference.getInt(mDevice.getAddress(), 648 PHONEBOOK_ACCESS_UNKNOWN); 649 } 650 651 } 652