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