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