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