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.settingslib.bluetooth; 18 19 import android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothClass; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothHearingAid; 23 import android.bluetooth.BluetoothProfile; 24 import android.bluetooth.BluetoothUuid; 25 import android.content.Context; 26 import android.content.SharedPreferences; 27 import android.content.res.Resources; 28 import android.graphics.drawable.BitmapDrawable; 29 import android.graphics.drawable.Drawable; 30 import android.net.Uri; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.os.Message; 34 import android.os.ParcelUuid; 35 import android.os.SystemClock; 36 import android.text.TextUtils; 37 import android.util.EventLog; 38 import android.util.Log; 39 import android.util.LruCache; 40 import android.util.Pair; 41 42 import androidx.annotation.VisibleForTesting; 43 44 import com.android.internal.util.ArrayUtils; 45 import com.android.settingslib.R; 46 import com.android.settingslib.Utils; 47 import com.android.settingslib.utils.ThreadUtils; 48 import com.android.settingslib.widget.AdaptiveOutlineDrawable; 49 50 import java.util.ArrayList; 51 import java.util.Collection; 52 import java.util.List; 53 import java.util.concurrent.CopyOnWriteArrayList; 54 55 /** 56 * CachedBluetoothDevice represents a remote Bluetooth device. It contains 57 * attributes of the device (such as the address, name, RSSI, etc.) and 58 * functionality that can be performed on the device (connect, pair, disconnect, 59 * etc.). 60 */ 61 public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { 62 private static final String TAG = "CachedBluetoothDevice"; 63 64 // See mConnectAttempted 65 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000; 66 // Some Hearing Aids (especially the 2nd device) needs more time to do service discovery 67 private static final long MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT = 15000; 68 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000; 69 private static final long MAX_MEDIA_PROFILE_CONNECT_DELAY = 60000; 70 71 private final Context mContext; 72 private final BluetoothAdapter mLocalAdapter; 73 private final LocalBluetoothProfileManager mProfileManager; 74 private final Object mProfileLock = new Object(); 75 BluetoothDevice mDevice; 76 private long mHiSyncId; 77 // Need this since there is no method for getting RSSI 78 short mRssi; 79 // mProfiles and mRemovedProfiles does not do swap() between main and sub device. It is 80 // because current sub device is only for HearingAid and its profile is the same. 81 private final Collection<LocalBluetoothProfile> mProfiles = new CopyOnWriteArrayList<>(); 82 83 // List of profiles that were previously in mProfiles, but have been removed 84 private final Collection<LocalBluetoothProfile> mRemovedProfiles = new CopyOnWriteArrayList<>(); 85 86 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP 87 private boolean mLocalNapRoleConnected; 88 89 boolean mJustDiscovered; 90 91 private final Collection<Callback> mCallbacks = new CopyOnWriteArrayList<>(); 92 93 /** 94 * Last time a bt profile auto-connect was attempted. 95 * If an ACTION_UUID intent comes in within 96 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect 97 * again with the new UUIDs 98 */ 99 private long mConnectAttempted; 100 101 // Active device state 102 private boolean mIsActiveDeviceA2dp = false; 103 private boolean mIsActiveDeviceHeadset = false; 104 private boolean mIsActiveDeviceHearingAid = false; 105 // Media profile connect state 106 private boolean mIsA2dpProfileConnectedFail = false; 107 private boolean mIsHeadsetProfileConnectedFail = false; 108 private boolean mIsHearingAidProfileConnectedFail = false; 109 private boolean mUnpairing = false; 110 // Group second device for Hearing Aid 111 private CachedBluetoothDevice mSubDevice; 112 @VisibleForTesting 113 LruCache<String, BitmapDrawable> mDrawableCache; 114 115 private final Handler mHandler = new Handler(Looper.getMainLooper()) { 116 @Override 117 public void handleMessage(Message msg) { 118 switch (msg.what) { 119 case BluetoothProfile.A2DP: 120 mIsA2dpProfileConnectedFail = true; 121 break; 122 case BluetoothProfile.HEADSET: 123 mIsHeadsetProfileConnectedFail = true; 124 break; 125 case BluetoothProfile.HEARING_AID: 126 mIsHearingAidProfileConnectedFail = true; 127 break; 128 default: 129 Log.w(TAG, "handleMessage(): unknown message : " + msg.what); 130 break; 131 } 132 Log.w(TAG, "Connect to profile : " + msg.what + " timeout, show error message !"); 133 refresh(); 134 } 135 }; 136 CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, BluetoothDevice device)137 CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, 138 BluetoothDevice device) { 139 mContext = context; 140 mLocalAdapter = BluetoothAdapter.getDefaultAdapter(); 141 mProfileManager = profileManager; 142 mDevice = device; 143 fillData(); 144 mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID; 145 initDrawableCache(); 146 } 147 initDrawableCache()148 private void initDrawableCache() { 149 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); 150 int cacheSize = maxMemory / 8; 151 152 mDrawableCache = new LruCache<String, BitmapDrawable>(cacheSize) { 153 @Override 154 protected int sizeOf(String key, BitmapDrawable bitmap) { 155 return bitmap.getBitmap().getByteCount() / 1024; 156 } 157 }; 158 } 159 160 /** 161 * Describes the current device and profile for logging. 162 * 163 * @param profile Profile to describe 164 * @return Description of the device and profile 165 */ describe(LocalBluetoothProfile profile)166 private String describe(LocalBluetoothProfile profile) { 167 StringBuilder sb = new StringBuilder(); 168 sb.append("Address:").append(mDevice); 169 if (profile != null) { 170 sb.append(" Profile:").append(profile); 171 } 172 173 return sb.toString(); 174 } 175 onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState)176 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) { 177 if (BluetoothUtils.D) { 178 Log.d(TAG, "onProfileStateChanged: profile " + profile + ", device " 179 + mDevice.getAlias() + ", newProfileState " + newProfileState); 180 } 181 if (mLocalAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF) 182 { 183 if (BluetoothUtils.D) { 184 Log.d(TAG, " BT Turninig Off...Profile conn state change ignored..."); 185 } 186 return; 187 } 188 189 synchronized (mProfileLock) { 190 if (profile instanceof A2dpProfile || profile instanceof HeadsetProfile 191 || profile instanceof HearingAidProfile) { 192 setProfileConnectedStatus(profile.getProfileId(), false); 193 switch (newProfileState) { 194 case BluetoothProfile.STATE_CONNECTED: 195 mHandler.removeMessages(profile.getProfileId()); 196 break; 197 case BluetoothProfile.STATE_CONNECTING: 198 mHandler.sendEmptyMessageDelayed(profile.getProfileId(), 199 MAX_MEDIA_PROFILE_CONNECT_DELAY); 200 break; 201 case BluetoothProfile.STATE_DISCONNECTING: 202 if (mHandler.hasMessages(profile.getProfileId())) { 203 mHandler.removeMessages(profile.getProfileId()); 204 } 205 break; 206 case BluetoothProfile.STATE_DISCONNECTED: 207 if (mHandler.hasMessages(profile.getProfileId())) { 208 mHandler.removeMessages(profile.getProfileId()); 209 setProfileConnectedStatus(profile.getProfileId(), true); 210 } 211 break; 212 default: 213 Log.w(TAG, "onProfileStateChanged(): unknown profile state : " 214 + newProfileState); 215 break; 216 } 217 } 218 219 if (newProfileState == BluetoothProfile.STATE_CONNECTED) { 220 if (profile instanceof MapProfile) { 221 profile.setEnabled(mDevice, true); 222 } 223 if (!mProfiles.contains(profile)) { 224 mRemovedProfiles.remove(profile); 225 mProfiles.add(profile); 226 if (profile instanceof PanProfile 227 && ((PanProfile) profile).isLocalRoleNap(mDevice)) { 228 // Device doesn't support NAP, so remove PanProfile on disconnect 229 mLocalNapRoleConnected = true; 230 } 231 } 232 } else if (profile instanceof MapProfile 233 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 234 profile.setEnabled(mDevice, false); 235 } else if (mLocalNapRoleConnected && profile instanceof PanProfile 236 && ((PanProfile) profile).isLocalRoleNap(mDevice) 237 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 238 Log.d(TAG, "Removing PanProfile from device after NAP disconnect"); 239 mProfiles.remove(profile); 240 mRemovedProfiles.add(profile); 241 mLocalNapRoleConnected = false; 242 } 243 } 244 245 fetchActiveDevices(); 246 } 247 248 @VisibleForTesting setProfileConnectedStatus(int profileId, boolean isFailed)249 void setProfileConnectedStatus(int profileId, boolean isFailed) { 250 switch (profileId) { 251 case BluetoothProfile.A2DP: 252 mIsA2dpProfileConnectedFail = isFailed; 253 break; 254 case BluetoothProfile.HEADSET: 255 mIsHeadsetProfileConnectedFail = isFailed; 256 break; 257 case BluetoothProfile.HEARING_AID: 258 mIsHearingAidProfileConnectedFail = isFailed; 259 break; 260 default: 261 Log.w(TAG, "setProfileConnectedStatus(): unknown profile id : " + profileId); 262 break; 263 } 264 } 265 disconnect()266 public void disconnect() { 267 synchronized (mProfileLock) { 268 mLocalAdapter.disconnectAllEnabledProfiles(mDevice); 269 } 270 // Disconnect PBAP server in case its connected 271 // This is to ensure all the profiles are disconnected as some CK/Hs do not 272 // disconnect PBAP connection when HF connection is brought down 273 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile(); 274 if (PbapProfile != null && isConnectedProfile(PbapProfile)) 275 { 276 PbapProfile.setEnabled(mDevice, false); 277 } 278 } 279 disconnect(LocalBluetoothProfile profile)280 public void disconnect(LocalBluetoothProfile profile) { 281 if (profile.setEnabled(mDevice, false)) { 282 if (BluetoothUtils.D) { 283 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile)); 284 } 285 } 286 } 287 288 /** 289 * Connect this device. 290 * 291 * @param connectAllProfiles {@code true} to connect all profile, {@code false} otherwise. 292 * 293 * @deprecated use {@link #connect()} instead. 294 */ 295 @Deprecated connect(boolean connectAllProfiles)296 public void connect(boolean connectAllProfiles) { 297 connect(); 298 } 299 300 /** 301 * Connect this device. 302 */ connect()303 public void connect() { 304 if (!ensurePaired()) { 305 return; 306 } 307 308 mConnectAttempted = SystemClock.elapsedRealtime(); 309 connectAllEnabledProfiles(); 310 } 311 getHiSyncId()312 public long getHiSyncId() { 313 return mHiSyncId; 314 } 315 setHiSyncId(long id)316 public void setHiSyncId(long id) { 317 mHiSyncId = id; 318 } 319 isHearingAidDevice()320 public boolean isHearingAidDevice() { 321 return mHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID; 322 } 323 onBondingDockConnect()324 void onBondingDockConnect() { 325 // Attempt to connect if UUIDs are available. Otherwise, 326 // we will connect when the ACTION_UUID intent arrives. 327 connect(); 328 } 329 connectAllEnabledProfiles()330 private void connectAllEnabledProfiles() { 331 synchronized (mProfileLock) { 332 // Try to initialize the profiles if they were not. 333 if (mProfiles.isEmpty()) { 334 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race 335 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been 336 // updated from bluetooth stack but ACTION.uuid is not sent yet. 337 // Eventually ACTION.uuid will be received which shall trigger the connection of the 338 // various profiles 339 // If UUIDs are not available yet, connect will be happen 340 // upon arrival of the ACTION_UUID intent. 341 Log.d(TAG, "No profiles. Maybe we will connect later for device " + mDevice); 342 return; 343 } 344 345 mLocalAdapter.connectAllEnabledProfiles(mDevice); 346 } 347 } 348 349 /** 350 * Connect this device to the specified profile. 351 * 352 * @param profile the profile to use with the remote device 353 */ connectProfile(LocalBluetoothProfile profile)354 public void connectProfile(LocalBluetoothProfile profile) { 355 mConnectAttempted = SystemClock.elapsedRealtime(); 356 connectInt(profile); 357 // Refresh the UI based on profile.connect() call 358 refresh(); 359 } 360 connectInt(LocalBluetoothProfile profile)361 synchronized void connectInt(LocalBluetoothProfile profile) { 362 if (!ensurePaired()) { 363 return; 364 } 365 if (profile.setEnabled(mDevice, true)) { 366 if (BluetoothUtils.D) { 367 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile)); 368 } 369 return; 370 } 371 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + getName()); 372 } 373 ensurePaired()374 private boolean ensurePaired() { 375 if (getBondState() == BluetoothDevice.BOND_NONE) { 376 startPairing(); 377 return false; 378 } else { 379 return true; 380 } 381 } 382 startPairing()383 public boolean startPairing() { 384 // Pairing is unreliable while scanning, so cancel discovery 385 if (mLocalAdapter.isDiscovering()) { 386 mLocalAdapter.cancelDiscovery(); 387 } 388 389 if (!mDevice.createBond()) { 390 return false; 391 } 392 393 return true; 394 } 395 unpair()396 public void unpair() { 397 int state = getBondState(); 398 399 if (state == BluetoothDevice.BOND_BONDING) { 400 mDevice.cancelBondProcess(); 401 } 402 403 if (state != BluetoothDevice.BOND_NONE) { 404 final BluetoothDevice dev = mDevice; 405 if (dev != null) { 406 mUnpairing = true; 407 final boolean successful = dev.removeBond(); 408 if (successful) { 409 releaseLruCache(); 410 if (BluetoothUtils.D) { 411 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null)); 412 } 413 } else if (BluetoothUtils.V) { 414 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " + 415 describe(null)); 416 } 417 } 418 } 419 } 420 getProfileConnectionState(LocalBluetoothProfile profile)421 public int getProfileConnectionState(LocalBluetoothProfile profile) { 422 return profile != null 423 ? profile.getConnectionStatus(mDevice) 424 : BluetoothProfile.STATE_DISCONNECTED; 425 } 426 427 // TODO: do any of these need to run async on a background thread? fillData()428 private void fillData() { 429 updateProfiles(); 430 fetchActiveDevices(); 431 migratePhonebookPermissionChoice(); 432 migrateMessagePermissionChoice(); 433 434 dispatchAttributesChanged(); 435 } 436 getDevice()437 public BluetoothDevice getDevice() { 438 return mDevice; 439 } 440 441 /** 442 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which 443 * causes problems in tests since BluetoothDevice is final and cannot be mocked. 444 * @return the address of this device 445 */ getAddress()446 public String getAddress() { 447 return mDevice.getAddress(); 448 } 449 450 /** 451 * Get name from remote device 452 * @return {@link BluetoothDevice#getAlias()} if 453 * {@link BluetoothDevice#getAlias()} is not null otherwise return 454 * {@link BluetoothDevice#getAddress()} 455 */ getName()456 public String getName() { 457 final String aliasName = mDevice.getAlias(); 458 return TextUtils.isEmpty(aliasName) ? getAddress() : aliasName; 459 } 460 461 /** 462 * User changes the device name 463 * @param name new alias name to be set, should never be null 464 */ setName(String name)465 public void setName(String name) { 466 // Prevent getName() to be set to null if setName(null) is called 467 if (name != null && !TextUtils.equals(name, getName())) { 468 mDevice.setAlias(name); 469 dispatchAttributesChanged(); 470 } 471 } 472 473 /** 474 * Set this device as active device 475 * @return true if at least one profile on this device is set to active, false otherwise 476 */ setActive()477 public boolean setActive() { 478 boolean result = false; 479 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 480 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) { 481 if (a2dpProfile.setActiveDevice(getDevice())) { 482 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this); 483 result = true; 484 } 485 } 486 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 487 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) { 488 if (headsetProfile.setActiveDevice(getDevice())) { 489 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this); 490 result = true; 491 } 492 } 493 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 494 if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) { 495 if (hearingAidProfile.setActiveDevice(getDevice())) { 496 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this); 497 result = true; 498 } 499 } 500 return result; 501 } 502 refreshName()503 void refreshName() { 504 if (BluetoothUtils.D) { 505 Log.d(TAG, "Device name: " + getName()); 506 } 507 dispatchAttributesChanged(); 508 } 509 510 /** 511 * Checks if device has a human readable name besides MAC address 512 * @return true if device's alias name is not null nor empty, false otherwise 513 */ hasHumanReadableName()514 public boolean hasHumanReadableName() { 515 return !TextUtils.isEmpty(mDevice.getAlias()); 516 } 517 518 /** 519 * Get battery level from remote device 520 * @return battery level in percentage [0-100], 521 * {@link BluetoothDevice#BATTERY_LEVEL_BLUETOOTH_OFF}, or 522 * {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} 523 */ getBatteryLevel()524 public int getBatteryLevel() { 525 return mDevice.getBatteryLevel(); 526 } 527 refresh()528 void refresh() { 529 ThreadUtils.postOnBackgroundThread(() -> { 530 if (BluetoothUtils.isAdvancedDetailsHeader(mDevice)) { 531 Uri uri = BluetoothUtils.getUriMetaData(getDevice(), 532 BluetoothDevice.METADATA_MAIN_ICON); 533 if (uri != null && mDrawableCache.get(uri.toString()) == null) { 534 mDrawableCache.put(uri.toString(), 535 (BitmapDrawable) BluetoothUtils.getBtDrawableWithDescription( 536 mContext, this).first); 537 } 538 } 539 540 ThreadUtils.postOnMainThread(() -> { 541 dispatchAttributesChanged(); 542 }); 543 }); 544 } 545 setJustDiscovered(boolean justDiscovered)546 public void setJustDiscovered(boolean justDiscovered) { 547 if (mJustDiscovered != justDiscovered) { 548 mJustDiscovered = justDiscovered; 549 dispatchAttributesChanged(); 550 } 551 } 552 getBondState()553 public int getBondState() { 554 return mDevice.getBondState(); 555 } 556 557 /** 558 * Update the device status as active or non-active per Bluetooth profile. 559 * 560 * @param isActive true if the device is active 561 * @param bluetoothProfile the Bluetooth profile 562 */ onActiveDeviceChanged(boolean isActive, int bluetoothProfile)563 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) { 564 boolean changed = false; 565 switch (bluetoothProfile) { 566 case BluetoothProfile.A2DP: 567 changed = (mIsActiveDeviceA2dp != isActive); 568 mIsActiveDeviceA2dp = isActive; 569 break; 570 case BluetoothProfile.HEADSET: 571 changed = (mIsActiveDeviceHeadset != isActive); 572 mIsActiveDeviceHeadset = isActive; 573 break; 574 case BluetoothProfile.HEARING_AID: 575 changed = (mIsActiveDeviceHearingAid != isActive); 576 mIsActiveDeviceHearingAid = isActive; 577 break; 578 default: 579 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile + 580 " isActive " + isActive); 581 break; 582 } 583 if (changed) { 584 dispatchAttributesChanged(); 585 } 586 } 587 588 /** 589 * Update the profile audio state. 590 */ onAudioModeChanged()591 void onAudioModeChanged() { 592 dispatchAttributesChanged(); 593 } 594 /** 595 * Get the device status as active or non-active per Bluetooth profile. 596 * 597 * @param bluetoothProfile the Bluetooth profile 598 * @return true if the device is active 599 */ 600 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) isActiveDevice(int bluetoothProfile)601 public boolean isActiveDevice(int bluetoothProfile) { 602 switch (bluetoothProfile) { 603 case BluetoothProfile.A2DP: 604 return mIsActiveDeviceA2dp; 605 case BluetoothProfile.HEADSET: 606 return mIsActiveDeviceHeadset; 607 case BluetoothProfile.HEARING_AID: 608 return mIsActiveDeviceHearingAid; 609 default: 610 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile); 611 break; 612 } 613 return false; 614 } 615 setRssi(short rssi)616 void setRssi(short rssi) { 617 if (mRssi != rssi) { 618 mRssi = rssi; 619 dispatchAttributesChanged(); 620 } 621 } 622 623 /** 624 * Checks whether we are connected to this device (any profile counts). 625 * 626 * @return Whether it is connected. 627 */ isConnected()628 public boolean isConnected() { 629 synchronized (mProfileLock) { 630 for (LocalBluetoothProfile profile : mProfiles) { 631 int status = getProfileConnectionState(profile); 632 if (status == BluetoothProfile.STATE_CONNECTED) { 633 return true; 634 } 635 } 636 637 return false; 638 } 639 } 640 isConnectedProfile(LocalBluetoothProfile profile)641 public boolean isConnectedProfile(LocalBluetoothProfile profile) { 642 int status = getProfileConnectionState(profile); 643 return status == BluetoothProfile.STATE_CONNECTED; 644 645 } 646 isBusy()647 public boolean isBusy() { 648 synchronized (mProfileLock) { 649 for (LocalBluetoothProfile profile : mProfiles) { 650 int status = getProfileConnectionState(profile); 651 if (status == BluetoothProfile.STATE_CONNECTING 652 || status == BluetoothProfile.STATE_DISCONNECTING) { 653 return true; 654 } 655 } 656 return getBondState() == BluetoothDevice.BOND_BONDING; 657 } 658 } 659 updateProfiles()660 private boolean updateProfiles() { 661 ParcelUuid[] uuids = mDevice.getUuids(); 662 if (uuids == null) return false; 663 664 ParcelUuid[] localUuids = mLocalAdapter.getUuids(); 665 if (localUuids == null) return false; 666 667 /* 668 * Now we know if the device supports PBAP, update permissions... 669 */ 670 processPhonebookAccess(); 671 672 synchronized (mProfileLock) { 673 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles, 674 mLocalNapRoleConnected, mDevice); 675 } 676 677 if (BluetoothUtils.D) { 678 Log.d(TAG, "updating profiles for " + mDevice.getAlias()); 679 BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); 680 681 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString()); 682 Log.v(TAG, "UUID:"); 683 for (ParcelUuid uuid : uuids) { 684 Log.v(TAG, " " + uuid); 685 } 686 } 687 return true; 688 } 689 fetchActiveDevices()690 private void fetchActiveDevices() { 691 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 692 if (a2dpProfile != null) { 693 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice()); 694 } 695 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 696 if (headsetProfile != null) { 697 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice()); 698 } 699 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 700 if (hearingAidProfile != null) { 701 mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice); 702 } 703 } 704 705 /** 706 * Refreshes the UI when framework alerts us of a UUID change. 707 */ onUuidChanged()708 void onUuidChanged() { 709 updateProfiles(); 710 ParcelUuid[] uuids = mDevice.getUuids(); 711 712 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT; 713 if (ArrayUtils.contains(uuids, BluetoothUuid.HOGP)) { 714 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT; 715 } else if (ArrayUtils.contains(uuids, BluetoothUuid.HEARING_AID)) { 716 timeout = MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT; 717 } 718 719 if (BluetoothUtils.D) { 720 Log.d(TAG, "onUuidChanged: Time since last connect=" 721 + (SystemClock.elapsedRealtime() - mConnectAttempted)); 722 } 723 724 /* 725 * If a connect was attempted earlier without any UUID, we will do the connect now. 726 * Otherwise, allow the connect on UUID change. 727 */ 728 if ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime()) { 729 Log.d(TAG, "onUuidChanged: triggering connectAllEnabledProfiles"); 730 connectAllEnabledProfiles(); 731 } 732 733 dispatchAttributesChanged(); 734 } 735 onBondingStateChanged(int bondState)736 void onBondingStateChanged(int bondState) { 737 if (bondState == BluetoothDevice.BOND_NONE) { 738 synchronized (mProfileLock) { 739 mProfiles.clear(); 740 } 741 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 742 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 743 mDevice.setSimAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 744 } 745 746 refresh(); 747 748 if (bondState == BluetoothDevice.BOND_BONDED && mDevice.isBondingInitiatedLocally()) { 749 connect(); 750 } 751 } 752 getBtClass()753 public BluetoothClass getBtClass() { 754 return mDevice.getBluetoothClass(); 755 } 756 getProfiles()757 public List<LocalBluetoothProfile> getProfiles() { 758 return new ArrayList<>(mProfiles); 759 } 760 getConnectableProfiles()761 public List<LocalBluetoothProfile> getConnectableProfiles() { 762 List<LocalBluetoothProfile> connectableProfiles = 763 new ArrayList<LocalBluetoothProfile>(); 764 synchronized (mProfileLock) { 765 for (LocalBluetoothProfile profile : mProfiles) { 766 if (profile.accessProfileEnabled()) { 767 connectableProfiles.add(profile); 768 } 769 } 770 } 771 return connectableProfiles; 772 } 773 getRemovedProfiles()774 public List<LocalBluetoothProfile> getRemovedProfiles() { 775 return new ArrayList<>(mRemovedProfiles); 776 } 777 registerCallback(Callback callback)778 public void registerCallback(Callback callback) { 779 mCallbacks.add(callback); 780 } 781 unregisterCallback(Callback callback)782 public void unregisterCallback(Callback callback) { 783 mCallbacks.remove(callback); 784 } 785 dispatchAttributesChanged()786 void dispatchAttributesChanged() { 787 for (Callback callback : mCallbacks) { 788 callback.onDeviceAttributesChanged(); 789 } 790 } 791 792 @Override toString()793 public String toString() { 794 return mDevice.toString(); 795 } 796 797 @Override equals(Object o)798 public boolean equals(Object o) { 799 if ((o == null) || !(o instanceof CachedBluetoothDevice)) { 800 return false; 801 } 802 return mDevice.equals(((CachedBluetoothDevice) o).mDevice); 803 } 804 805 @Override hashCode()806 public int hashCode() { 807 return mDevice.getAddress().hashCode(); 808 } 809 810 // This comparison uses non-final fields so the sort order may change 811 // when device attributes change (such as bonding state). Settings 812 // will completely refresh the device list when this happens. compareTo(CachedBluetoothDevice another)813 public int compareTo(CachedBluetoothDevice another) { 814 // Connected above not connected 815 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0); 816 if (comparison != 0) return comparison; 817 818 // Paired above not paired 819 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) - 820 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0); 821 if (comparison != 0) return comparison; 822 823 // Just discovered above discovered in the past 824 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0); 825 if (comparison != 0) return comparison; 826 827 // Stronger signal above weaker signal 828 comparison = another.mRssi - mRssi; 829 if (comparison != 0) return comparison; 830 831 // Fallback on name 832 return getName().compareTo(another.getName()); 833 } 834 835 public interface Callback { onDeviceAttributesChanged()836 void onDeviceAttributesChanged(); 837 } 838 839 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 840 // app's shared preferences). migratePhonebookPermissionChoice()841 private void migratePhonebookPermissionChoice() { 842 SharedPreferences preferences = mContext.getSharedPreferences( 843 "bluetooth_phonebook_permission", Context.MODE_PRIVATE); 844 if (!preferences.contains(mDevice.getAddress())) { 845 return; 846 } 847 848 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 849 int oldPermission = 850 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN); 851 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) { 852 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 853 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) { 854 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 855 } 856 } 857 858 SharedPreferences.Editor editor = preferences.edit(); 859 editor.remove(mDevice.getAddress()); 860 editor.commit(); 861 } 862 863 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 864 // app's shared preferences). migrateMessagePermissionChoice()865 private void migrateMessagePermissionChoice() { 866 SharedPreferences preferences = mContext.getSharedPreferences( 867 "bluetooth_message_permission", Context.MODE_PRIVATE); 868 if (!preferences.contains(mDevice.getAddress())) { 869 return; 870 } 871 872 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 873 int oldPermission = 874 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN); 875 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) { 876 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 877 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) { 878 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 879 } 880 } 881 882 SharedPreferences.Editor editor = preferences.edit(); 883 editor.remove(mDevice.getAddress()); 884 editor.commit(); 885 } 886 processPhonebookAccess()887 private void processPhonebookAccess() { 888 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return; 889 890 ParcelUuid[] uuids = mDevice.getUuids(); 891 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) { 892 // The pairing dialog now warns of phone-book access for paired devices. 893 // No separate prompt is displayed after pairing. 894 final BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); 895 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 896 if (bluetoothClass != null && (bluetoothClass.getDeviceClass() 897 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE 898 || bluetoothClass.getDeviceClass() 899 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET)) { 900 EventLog.writeEvent(0x534e4554, "138529441", -1, ""); 901 } 902 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 903 } 904 } 905 } 906 getMaxConnectionState()907 public int getMaxConnectionState() { 908 int maxState = BluetoothProfile.STATE_DISCONNECTED; 909 synchronized (mProfileLock) { 910 for (LocalBluetoothProfile profile : getProfiles()) { 911 int connectionStatus = getProfileConnectionState(profile); 912 if (connectionStatus > maxState) { 913 maxState = connectionStatus; 914 } 915 } 916 } 917 return maxState; 918 } 919 920 /** 921 * Return full summary that describes connection state of this device 922 * 923 * @see #getConnectionSummary(boolean shortSummary) 924 */ getConnectionSummary()925 public String getConnectionSummary() { 926 return getConnectionSummary(false /* shortSummary */); 927 } 928 929 /** 930 * Return summary that describes connection state of this device. Summary depends on: 931 * 1. Whether device has battery info 932 * 2. Whether device is in active usage(or in phone call) 933 * 934 * @param shortSummary {@code true} if need to return short version summary 935 */ getConnectionSummary(boolean shortSummary)936 public String getConnectionSummary(boolean shortSummary) { 937 boolean profileConnected = false; // Updated as long as BluetoothProfile is connected 938 boolean a2dpConnected = true; // A2DP is connected 939 boolean hfpConnected = true; // HFP is connected 940 boolean hearingAidConnected = true; // Hearing Aid is connected 941 int leftBattery = -1; 942 int rightBattery = -1; 943 944 if (isProfileConnectedFail() && isConnected()) { 945 return mContext.getString(R.string.profile_connect_timeout_subtext); 946 } 947 948 synchronized (mProfileLock) { 949 for (LocalBluetoothProfile profile : getProfiles()) { 950 int connectionStatus = getProfileConnectionState(profile); 951 952 switch (connectionStatus) { 953 case BluetoothProfile.STATE_CONNECTING: 954 case BluetoothProfile.STATE_DISCONNECTING: 955 return mContext.getString( 956 BluetoothUtils.getConnectionStateSummary(connectionStatus)); 957 958 case BluetoothProfile.STATE_CONNECTED: 959 profileConnected = true; 960 break; 961 962 case BluetoothProfile.STATE_DISCONNECTED: 963 if (profile.isProfileReady()) { 964 if (profile instanceof A2dpProfile 965 || profile instanceof A2dpSinkProfile) { 966 a2dpConnected = false; 967 } else if (profile instanceof HeadsetProfile 968 || profile instanceof HfpClientProfile) { 969 hfpConnected = false; 970 } else if (profile instanceof HearingAidProfile) { 971 hearingAidConnected = false; 972 } 973 } 974 break; 975 } 976 } 977 } 978 979 String batteryLevelPercentageString = null; 980 // Android framework should only set mBatteryLevel to valid range [0-100], 981 // BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF, or BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 982 // any other value should be a framework bug. Thus assume here that if value is greater 983 // than BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must be valid 984 final int batteryLevel = getBatteryLevel(); 985 if (batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 986 // TODO: name com.android.settingslib.bluetooth.Utils something different 987 batteryLevelPercentageString = 988 com.android.settingslib.Utils.formatPercentage(batteryLevel); 989 } 990 991 int stringRes = R.string.bluetooth_pairing; 992 //when profile is connected, information would be available 993 if (profileConnected) { 994 // Update Meta data for connected device 995 if (BluetoothUtils.getBooleanMetaData( 996 mDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) { 997 leftBattery = BluetoothUtils.getIntMetaData(mDevice, 998 BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY); 999 rightBattery = BluetoothUtils.getIntMetaData(mDevice, 1000 BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY); 1001 } 1002 1003 // Set default string with battery level in device connected situation. 1004 if (isTwsBatteryAvailable(leftBattery, rightBattery)) { 1005 stringRes = R.string.bluetooth_battery_level_untethered; 1006 } else if (batteryLevelPercentageString != null) { 1007 stringRes = R.string.bluetooth_battery_level; 1008 } 1009 1010 // Set active string in following device connected situation. 1011 // 1. Hearing Aid device active. 1012 // 2. Headset device active with in-calling state. 1013 // 3. A2DP device active without in-calling state. 1014 if (a2dpConnected || hfpConnected || hearingAidConnected) { 1015 final boolean isOnCall = Utils.isAudioModeOngoingCall(mContext); 1016 if ((mIsActiveDeviceHearingAid) 1017 || (mIsActiveDeviceHeadset && isOnCall) 1018 || (mIsActiveDeviceA2dp && !isOnCall)) { 1019 if (isTwsBatteryAvailable(leftBattery, rightBattery) && !shortSummary) { 1020 stringRes = R.string.bluetooth_active_battery_level_untethered; 1021 } else if (batteryLevelPercentageString != null && !shortSummary) { 1022 stringRes = R.string.bluetooth_active_battery_level; 1023 } else { 1024 stringRes = R.string.bluetooth_active_no_battery_level; 1025 } 1026 } 1027 } 1028 } 1029 1030 if (stringRes != R.string.bluetooth_pairing 1031 || getBondState() == BluetoothDevice.BOND_BONDING) { 1032 if (isTwsBatteryAvailable(leftBattery, rightBattery)) { 1033 return mContext.getString(stringRes, Utils.formatPercentage(leftBattery), 1034 Utils.formatPercentage(rightBattery)); 1035 } else { 1036 return mContext.getString(stringRes, batteryLevelPercentageString); 1037 } 1038 } else { 1039 return null; 1040 } 1041 } 1042 isTwsBatteryAvailable(int leftBattery, int rightBattery)1043 private boolean isTwsBatteryAvailable(int leftBattery, int rightBattery) { 1044 return leftBattery >= 0 && rightBattery >= 0; 1045 } 1046 isProfileConnectedFail()1047 private boolean isProfileConnectedFail() { 1048 return mIsA2dpProfileConnectedFail || mIsHearingAidProfileConnectedFail 1049 || (!isConnectedSapDevice() && mIsHeadsetProfileConnectedFail); 1050 } 1051 1052 /** 1053 * @return resource for android auto string that describes the connection state of this device. 1054 */ getCarConnectionSummary()1055 public String getCarConnectionSummary() { 1056 boolean profileConnected = false; // at least one profile is connected 1057 boolean a2dpNotConnected = false; // A2DP is preferred but not connected 1058 boolean hfpNotConnected = false; // HFP is preferred but not connected 1059 boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected 1060 1061 synchronized (mProfileLock) { 1062 for (LocalBluetoothProfile profile : getProfiles()) { 1063 int connectionStatus = getProfileConnectionState(profile); 1064 1065 switch (connectionStatus) { 1066 case BluetoothProfile.STATE_CONNECTING: 1067 case BluetoothProfile.STATE_DISCONNECTING: 1068 return mContext.getString( 1069 BluetoothUtils.getConnectionStateSummary(connectionStatus)); 1070 1071 case BluetoothProfile.STATE_CONNECTED: 1072 profileConnected = true; 1073 break; 1074 1075 case BluetoothProfile.STATE_DISCONNECTED: 1076 if (profile.isProfileReady()) { 1077 if (profile instanceof A2dpProfile 1078 || profile instanceof A2dpSinkProfile) { 1079 a2dpNotConnected = true; 1080 } else if (profile instanceof HeadsetProfile 1081 || profile instanceof HfpClientProfile) { 1082 hfpNotConnected = true; 1083 } else if (profile instanceof HearingAidProfile) { 1084 hearingAidNotConnected = true; 1085 } 1086 } 1087 break; 1088 } 1089 } 1090 } 1091 1092 String batteryLevelPercentageString = null; 1093 // Android framework should only set mBatteryLevel to valid range [0-100], 1094 // BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF, or BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1095 // any other value should be a framework bug. Thus assume here that if value is greater 1096 // than BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must be valid 1097 final int batteryLevel = getBatteryLevel(); 1098 if (batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 1099 // TODO: name com.android.settingslib.bluetooth.Utils something different 1100 batteryLevelPercentageString = 1101 com.android.settingslib.Utils.formatPercentage(batteryLevel); 1102 } 1103 1104 // Prepare the string for the Active Device summary 1105 String[] activeDeviceStringsArray = mContext.getResources().getStringArray( 1106 R.array.bluetooth_audio_active_device_summaries); 1107 String activeDeviceString = activeDeviceStringsArray[0]; // Default value: not active 1108 if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) { 1109 activeDeviceString = activeDeviceStringsArray[1]; // Active for Media and Phone 1110 } else { 1111 if (mIsActiveDeviceA2dp) { 1112 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only 1113 } 1114 if (mIsActiveDeviceHeadset) { 1115 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only 1116 } 1117 } 1118 if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) { 1119 activeDeviceString = activeDeviceStringsArray[1]; 1120 return mContext.getString(R.string.bluetooth_connected, activeDeviceString); 1121 } 1122 1123 if (profileConnected) { 1124 if (a2dpNotConnected && hfpNotConnected) { 1125 if (batteryLevelPercentageString != null) { 1126 return mContext.getString( 1127 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level, 1128 batteryLevelPercentageString, activeDeviceString); 1129 } else { 1130 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp, 1131 activeDeviceString); 1132 } 1133 1134 } else if (a2dpNotConnected) { 1135 if (batteryLevelPercentageString != null) { 1136 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level, 1137 batteryLevelPercentageString, activeDeviceString); 1138 } else { 1139 return mContext.getString(R.string.bluetooth_connected_no_a2dp, 1140 activeDeviceString); 1141 } 1142 1143 } else if (hfpNotConnected) { 1144 if (batteryLevelPercentageString != null) { 1145 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level, 1146 batteryLevelPercentageString, activeDeviceString); 1147 } else { 1148 return mContext.getString(R.string.bluetooth_connected_no_headset, 1149 activeDeviceString); 1150 } 1151 } else { 1152 if (batteryLevelPercentageString != null) { 1153 return mContext.getString(R.string.bluetooth_connected_battery_level, 1154 batteryLevelPercentageString, activeDeviceString); 1155 } else { 1156 return mContext.getString(R.string.bluetooth_connected, activeDeviceString); 1157 } 1158 } 1159 } 1160 1161 return getBondState() == BluetoothDevice.BOND_BONDING ? 1162 mContext.getString(R.string.bluetooth_pairing) : null; 1163 } 1164 1165 /** 1166 * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device 1167 */ isConnectedA2dpDevice()1168 public boolean isConnectedA2dpDevice() { 1169 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 1170 return a2dpProfile != null && a2dpProfile.getConnectionStatus(mDevice) == 1171 BluetoothProfile.STATE_CONNECTED; 1172 } 1173 1174 /** 1175 * @return {@code true} if {@code cachedBluetoothDevice} is HFP device 1176 */ isConnectedHfpDevice()1177 public boolean isConnectedHfpDevice() { 1178 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 1179 return headsetProfile != null && headsetProfile.getConnectionStatus(mDevice) == 1180 BluetoothProfile.STATE_CONNECTED; 1181 } 1182 1183 /** 1184 * @return {@code true} if {@code cachedBluetoothDevice} is Hearing Aid device 1185 */ isConnectedHearingAidDevice()1186 public boolean isConnectedHearingAidDevice() { 1187 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 1188 return hearingAidProfile != null && hearingAidProfile.getConnectionStatus(mDevice) == 1189 BluetoothProfile.STATE_CONNECTED; 1190 } 1191 isConnectedSapDevice()1192 private boolean isConnectedSapDevice() { 1193 SapProfile sapProfile = mProfileManager.getSapProfile(); 1194 return sapProfile != null && sapProfile.getConnectionStatus(mDevice) 1195 == BluetoothProfile.STATE_CONNECTED; 1196 } 1197 getSubDevice()1198 public CachedBluetoothDevice getSubDevice() { 1199 return mSubDevice; 1200 } 1201 setSubDevice(CachedBluetoothDevice subDevice)1202 public void setSubDevice(CachedBluetoothDevice subDevice) { 1203 mSubDevice = subDevice; 1204 } 1205 switchSubDeviceContent()1206 public void switchSubDeviceContent() { 1207 // Backup from main device 1208 BluetoothDevice tmpDevice = mDevice; 1209 short tmpRssi = mRssi; 1210 boolean tmpJustDiscovered = mJustDiscovered; 1211 // Set main device from sub device 1212 mDevice = mSubDevice.mDevice; 1213 mRssi = mSubDevice.mRssi; 1214 mJustDiscovered = mSubDevice.mJustDiscovered; 1215 // Set sub device from backup 1216 mSubDevice.mDevice = tmpDevice; 1217 mSubDevice.mRssi = tmpRssi; 1218 mSubDevice.mJustDiscovered = tmpJustDiscovered; 1219 fetchActiveDevices(); 1220 } 1221 1222 /** 1223 * Get cached bluetooth icon with description 1224 */ getDrawableWithDescription()1225 public Pair<Drawable, String> getDrawableWithDescription() { 1226 Uri uri = BluetoothUtils.getUriMetaData(mDevice, BluetoothDevice.METADATA_MAIN_ICON); 1227 Pair<Drawable, String> pair = BluetoothUtils.getBtClassDrawableWithDescription( 1228 mContext, this); 1229 1230 if (BluetoothUtils.isAdvancedDetailsHeader(mDevice) && uri != null) { 1231 BitmapDrawable drawable = mDrawableCache.get(uri.toString()); 1232 if (drawable != null) { 1233 Resources resources = mContext.getResources(); 1234 return new Pair<>(new AdaptiveOutlineDrawable( 1235 resources, drawable.getBitmap()), pair.second); 1236 } 1237 1238 refresh(); 1239 } 1240 1241 return new Pair<>(BluetoothUtils.buildBtRainbowDrawable( 1242 mContext, pair.first, getAddress().hashCode()), pair.second); 1243 } 1244 releaseLruCache()1245 void releaseLruCache() { 1246 mDrawableCache.evictAll(); 1247 } 1248 getUnpairing()1249 boolean getUnpairing() { 1250 return mUnpairing; 1251 } 1252 } 1253