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