• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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