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